diff --git a/ferrtable-test/src/main.rs b/ferrtable-test/src/main.rs index 40774d8..9f46240 100644 --- a/ferrtable-test/src/main.rs +++ b/ferrtable-test/src/main.rs @@ -1,3 +1,5 @@ +use std::error::Error; + use ferrtable::Client; use futures::StreamExt as _; use serde::{Deserialize, Serialize}; @@ -25,14 +27,14 @@ enum RecordStatus { } #[tokio::main] -async fn main() { - let settings = Settings::load().unwrap(); - let client = Client::new_from_access_token(&settings.access_token).unwrap(); +async fn main() -> Result<(), Box> { + let settings = Settings::load()?; + let client = Client::new_from_access_token(&settings.access_token)?; println!("Testing Client::list_bases()..."); - let mut bases = client.list_bases().build().unwrap().stream_items(); + let mut bases = client.list_bases().build()?.stream_items(); while let Some(res) = bases.next().await { - dbg!(res.unwrap()); + dbg!(res?); } println!("Testing Client::create_records()..."); @@ -44,28 +46,46 @@ async fn main() { }]) .with_base_id(settings.base_id.clone()) .with_table_id(settings.table_id.clone()) - .build() - .unwrap() + .build()? .execute() - .await - .unwrap(); + .await?; println!("Testing Client::list_records()..."); let records = client .list_records() .with_base_id(settings.base_id.clone()) .with_table_id(settings.table_id.clone()) - .build() - .unwrap() + .build()? .stream_items::() .collect::>() .await .into_iter() - .collect::, _>>() - .unwrap(); - for rec in records { - dbg!(rec.fields); + .collect::, _>>()?; + for rec in records.iter() { + dbg!(&rec.fields); } + let record = client + .get_record() + .with_base_id(settings.base_id.clone()) + .with_table_id(settings.table_id.clone()) + .with_record_id("does_not_exist".to_string()) + .build()? + .fetch_optional::() + .await?; + assert!(record.is_none()); + + let record = client + .get_record() + .with_base_id(settings.base_id.clone()) + .with_table_id(settings.table_id.clone()) + .with_record_id(records.first().unwrap().id.clone()) + .build()? + .fetch_optional::() + .await?; + dbg!(record); + println!("All tests succeeded."); + + Ok(()) } diff --git a/ferrtable/src/client.rs b/ferrtable/src/client.rs index 59b5d1c..fb2b8ce 100644 --- a/ferrtable/src/client.rs +++ b/ferrtable/src/client.rs @@ -5,8 +5,8 @@ use std::fmt::Debug; use serde::Serialize; use crate::{ - create_records::CreateRecordsQueryBuilder, list_bases::ListBasesQueryBuilder, - list_records::ListRecordsQueryBuilder, + create_records::CreateRecordsQueryBuilder, get_record::GetRecordQueryBuilder, + list_bases::ListBasesQueryBuilder, list_records::ListRecordsQueryBuilder, }; const DEFAULT_API_ROOT: &str = "https://api.airtable.com"; @@ -68,6 +68,32 @@ impl Client { .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 + /// let result = client + /// .get_record() + /// .with_base_id("***".to_owned()) + /// .with_table_id("***".to_owned()) + /// .with_record_id("***".to_owned()) + /// .build()? + /// .fetch_optional() + /// .await?; + /// dbg!(result); + /// ``` + pub fn get_record(&self) -> GetRecordQueryBuilder { + GetRecordQueryBuilder::default().with_client(self.clone()) + } + /// List the bases the token can access /// /// # Examples diff --git a/ferrtable/src/get_record.rs b/ferrtable/src/get_record.rs new file mode 100644 index 0000000..ce00233 --- /dev/null +++ b/ferrtable/src/get_record.rs @@ -0,0 +1,52 @@ +use std::fmt::Debug; + +use derive_builder::Builder; +use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode}; +use serde::{Serialize, de::DeserializeOwned}; + +use crate::client::Client; +use crate::errors::ExecutionError; +use crate::types::AirtableRecord; + +#[derive(Builder, Clone, Debug, Serialize)] +#[builder(pattern = "owned", setter(prefix = "with"))] +pub struct GetRecordQuery { + #[serde(skip)] + base_id: String, + + #[serde(skip)] + #[builder(vis = "pub(crate)")] + client: Client, + + #[serde(skip)] + record_id: String, + + #[serde(skip)] + table_id: String, +} + +impl GetRecordQuery { + pub async fn fetch_optional(self) -> Result>, ExecutionError> + where + T: Clone + Debug + DeserializeOwned, + { + 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 record_id = utf8_percent_encode(&self.record_id, NON_ALPHANUMERIC).to_string(); + let http_resp = self + .client + .get_path(&format!("v0/{base_id}/{table_id}/{record_id}")) + .send() + .await?; + match http_resp.error_for_status() { + Ok(http_resp) => Ok(http_resp.json().await?), + Err(err) => { + if err.status() == Some(reqwest::StatusCode::NOT_FOUND) { + Ok(None) + } else { + Err(err.into()) + } + } + } + } +} diff --git a/ferrtable/src/lib.rs b/ferrtable/src/lib.rs index c0d2af4..43c6977 100644 --- a/ferrtable/src/lib.rs +++ b/ferrtable/src/lib.rs @@ -91,6 +91,7 @@ pub mod types; // Each API operation is organized into a dedicated Rust module. pub mod create_records; +pub mod get_record; pub mod list_bases; pub mod list_records;