ferrtable 0.0.2

Ferris the crab's favorite Airtable library
Documentation
//! Main Ferrtable client used to make requests.

use std::fmt::Debug;

use serde::Serialize;

use crate::{
    create_records::CreateRecordsQueryBuilder, get_record::GetRecordQueryBuilder,
    list_bases::ListBasesQueryBuilder, list_records::ListRecordsQueryBuilder,
};

const DEFAULT_API_ROOT: &str = "https://api.airtable.com";

#[derive(Clone)]
pub struct Client {
    // TODO: Does API root need to be customizable per client, e.g. for on-prem
    // enterprise deployments, or is that not a thing?
    api_root: String,
    client: reqwest::Client,
    token: String,
}

impl Client {
    pub fn new_from_access_token(token: &str) -> Result<Self, reqwest::Error> {
        Ok(Self {
            api_root: DEFAULT_API_ROOT.to_owned(),
            client: reqwest::ClientBuilder::default()
                .https_only(true)
                .build()
                .expect("reqwest client is always built with the same configuration here"),
            token: token.to_owned(),
        })
    }

    /// Creates multiple records. Note that table names and table ids can be
    /// used interchangeably. We recommend using table IDs so you don't need
    /// to modify your API request when your table name changes.
    ///
    /// Your request body should include an array of up to 10 record objects.
    ///
    /// Returns a unique array of the newly created record ids if the call
    /// succeeds.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use std::collections::HashMap;
    /// # use ferrtable::Client;
    /// # #[tokio::main]
    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// # let client = Client::new_from_access_token("*****")?;
    /// client
    ///     .create_records([
    ///         HashMap::<String, String>::from([
    ///             ("name".to_owned(), "Steal Improbability Drive".to_owned()),
    ///             ("notes".to_owned(), "Just for fun, no other reason.".to_owned()),
    ///             ("status".to_owned(), "In progress".to_owned()),
    ///         ]),
    ///     ])
    ///     .with_base_id("***".to_owned())
    ///     .with_table_id("***".to_owned())
    ///     .build()?
    ///     .execute()
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn create_records<I, T>(&self, records: I) -> CreateRecordsQueryBuilder<T>
    where
        T: Serialize,
        I: IntoIterator<Item = T>,
    {
        CreateRecordsQueryBuilder::default()
            .with_client(self.clone())
            .with_records(records.into_iter().collect())
    }

    /// Retrieve a single record. Any "empty" fields (e.g. "", [], or false) in
    /// the record will not be returned.
    ///
    /// Note If we can't locate the record on a given table, the request will
    /// fallback to a base wide search and will still return the record if the
    /// Record ID is valid and the record is located within the same base.
    ///
    /// # Examples
    ///
    /// ## Basic Usage
    ///
    /// ```no_run
    /// # use std::collections::HashMap;
    /// # use ferrtable::Client;
    /// # #[tokio::main]
    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// # let client = Client::new_from_access_token("*****")?;
    /// let result = client
    ///     .get_record()
    ///     .with_base_id("***".to_owned())
    ///     .with_table_id("***".to_owned())
    ///     .with_record_id("***".to_owned())
    ///     .build()?
    ///     .fetch_optional::<HashMap<String, String>>()
    ///     .await?;
    /// dbg!(result);
    /// # Ok(())
    /// # }
    /// ```
    pub fn get_record(&self) -> GetRecordQueryBuilder {
        GetRecordQueryBuilder::default().with_client(self.clone())
    }

    /// List the bases the token can access
    ///
    /// # Examples
    ///
    /// ## Consuming as Stream
    ///
    /// ```no_run
    /// use futures::prelude::*;
    ///
    /// # use std::collections::HashMap;
    /// # use ferrtable::Client;
    /// # #[tokio::main]
    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// # let client = Client::new_from_access_token("*****")?;
    /// let mut base_stream = client
    ///     .list_bases()
    ///     .build()?
    ///     .stream_items();
    ///
    /// while let Some(result) = base_stream.next().await {
    ///     dbg!(result?);
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub fn list_bases(&self) -> ListBasesQueryBuilder {
        ListBasesQueryBuilder::default().with_client(self.clone())
    }

    /// List records in a table. Note that table names and table ids can be used
    /// interchangeably. We recommend using table IDs so you don't need to modify
    /// your API request when your table name changes.
    ///
    /// # Examples
    ///
    /// ## Consuming as Stream
    ///
    /// ```no_run
    /// use futures::prelude::*;
    ///
    /// # use std::collections::HashMap;
    /// # use ferrtable::Client;
    /// # #[tokio::main]
    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// # let client = Client::new_from_access_token("*****")?;
    /// let mut rec_stream = client
    ///     .list_records()
    ///     .with_base_id("***".to_owned())
    ///     .with_table_id("***".to_owned())
    ///     .build()?
    ///     .stream_items::<HashMap<String, serde_json::Value>>();
    ///
    /// while let Some(result) = rec_stream.next().await {
    ///     dbg!(result?.fields);
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub fn list_records(&self) -> ListRecordsQueryBuilder {
        ListRecordsQueryBuilder::default().with_client(self.clone())
    }

    /// Constructs a RequestBuilder with URL "{self.api_root}/{path}" and the
    /// Authorization header set to the correct bearer auth value.
    pub(crate) fn get_path(&self, path: &str) -> reqwest::RequestBuilder {
        let Self {
            api_root, token, ..
        } = self;
        self.client
            .get(format!("{api_root}/{path}"))
            .header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
    }

    /// Constructs a RequestBuilder with URL "{self.api_root}/{path}" and the
    /// Authorization header set to the correct bearer auth value.
    pub(crate) fn post_path(&self, path: &str) -> reqwest::RequestBuilder {
        let Self {
            api_root, token, ..
        } = self;
        self.client
            .post(format!("{api_root}/{path}"))
            .header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
    }
}

impl Debug for Client {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "ferrtable::Client {{ *** }}")
    }
}