From b2d1bdcaaca12fe49431a03ac957033397edb280 Mon Sep 17 00:00:00 2001 From: Brent Schroeter Date: Sun, 11 Jan 2026 20:42:42 +0000 Subject: [PATCH] evaluate build() fns implicitly when executing --- ferrtable-test/src/main.rs | 28 ++++++++++++--------------- ferrtable/README.md | 14 ++++++-------- ferrtable/src/client.rs | 22 +++++++++------------ ferrtable/src/create_records.rs | 29 ++++++++++++++++++++-------- ferrtable/src/errors.rs | 16 ++++++++++++++++ ferrtable/src/get_record.rs | 34 +++++++++++++++++++++++---------- ferrtable/src/lib.rs | 14 ++++++-------- ferrtable/src/list_bases.rs | 15 ++++++++++----- ferrtable/src/list_records.rs | 27 +++++++++++++++----------- 9 files changed, 120 insertions(+), 79 deletions(-) diff --git a/ferrtable-test/src/main.rs b/ferrtable-test/src/main.rs index 9f46240..e09061e 100644 --- a/ferrtable-test/src/main.rs +++ b/ferrtable-test/src/main.rs @@ -32,7 +32,7 @@ async fn main() -> Result<(), Box> { let client = Client::new_from_access_token(&settings.access_token)?; println!("Testing Client::list_bases()..."); - let mut bases = client.list_bases().build()?.stream_items(); + let mut bases = client.list_bases().stream_items()?; while let Some(res) = bases.next().await { dbg!(res?); } @@ -44,19 +44,17 @@ async fn main() -> Result<(), Box> { notes: Some("Just for fun, no other reason.".to_owned()), status: Some(RecordStatus::InProgress), }]) - .with_base_id(settings.base_id.clone()) - .with_table_id(settings.table_id.clone()) - .build()? + .with_base_id(&settings.base_id) + .with_table_id(&settings.table_id) .execute() .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()? - .stream_items::() + .with_base_id(&settings.base_id) + .with_table_id(&settings.table_id) + .stream_items::()? .collect::>() .await .into_iter() @@ -67,20 +65,18 @@ async fn main() -> Result<(), Box> { 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()? + .with_base_id(&settings.base_id) + .with_table_id(&settings.table_id) + .with_record_id("does_not_exist") .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()? + .with_base_id(&settings.base_id) + .with_table_id(&settings.table_id) + .with_record_id(&records.first().unwrap().id) .fetch_optional::() .await?; dbg!(record); diff --git a/ferrtable/README.md b/ferrtable/README.md index 53def24..e99071f 100644 --- a/ferrtable/README.md +++ b/ferrtable/README.md @@ -68,19 +68,17 @@ async fn main() -> Result<(), Box> { status: Status::InProgress, attachments: vec![], }]) - .with_base_id("***".to_owned()) - .with_table_id("***".to_owned()) - .build()? + .with_base_id("***") + .with_table_id("***") .execute() .await?; let mut rec_stream = client .list_records() - .with_base_id("***".to_owned()) - .with_table_id("***".to_owned()) - .with_filter("{status} = 'Todo' || {status} = 'In Progress'".to_owned()) - .build()? - .stream_items::(); + .with_base_id("***") + .with_table_id("***") + .with_filter("{status} = 'Todo' || {status} = 'In Progress'") + .stream_items::()?; while let Some(result) = rec_stream.next().await { dbg!(result?.fields); diff --git a/ferrtable/src/client.rs b/ferrtable/src/client.rs index c5b2ca1..4f3503e 100644 --- a/ferrtable/src/client.rs +++ b/ferrtable/src/client.rs @@ -57,9 +57,8 @@ impl Client { /// ("status".to_owned(), "In progress".to_owned()), /// ]), /// ]) - /// .with_base_id("***".to_owned()) - /// .with_table_id("***".to_owned()) - /// .build()? + /// .with_base_id("***") + /// .with_table_id("***") /// .execute() /// .await?; /// # Ok(()) @@ -94,10 +93,9 @@ impl Client { /// # 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()? + /// .with_base_id("***") + /// .with_table_id("***") + /// .with_record_id("***") /// .fetch_optional::>() /// .await?; /// dbg!(result); @@ -124,8 +122,7 @@ impl Client { /// # let client = Client::new_from_access_token("*****")?; /// let mut base_stream = client /// .list_bases() - /// .build()? - /// .stream_items(); + /// .stream_items()?; /// /// while let Some(result) = base_stream.next().await { /// dbg!(result?); @@ -155,10 +152,9 @@ impl Client { /// # 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::>(); + /// .with_base_id("***") + /// .with_table_id("***") + /// .stream_items::>()?; /// /// while let Some(result) = rec_stream.next().await { /// dbg!(result?.fields); diff --git a/ferrtable/src/create_records.rs b/ferrtable/src/create_records.rs index ccef799..4a07bd8 100644 --- a/ferrtable/src/create_records.rs +++ b/ferrtable/src/create_records.rs @@ -4,14 +4,23 @@ 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}; +use crate::{ + client::Client, + errors::{ExecutionError, Result}, + types::AirtableRecord, +}; #[derive(Builder, Clone, Debug)] -#[builder(pattern = "owned", setter(prefix = "with"))] +#[builder( + build_fn(error = "ExecutionError", private), + pattern = "owned", + setter(prefix = "with") +)] pub struct CreateRecordsQuery where T: Serialize, { + #[builder(setter(into))] base_id: String, #[builder(vis = "pub(crate)")] @@ -20,6 +29,7 @@ where #[builder(vis = "pub(crate)")] records: Vec, + #[builder(setter(into))] table_id: String, } @@ -44,7 +54,7 @@ pub struct CreateRecordsDetails { pub reasons: Vec, } -impl CreateRecordsQuery +impl CreateRecordsQueryBuilder where T: Clone + Debug + DeserializeOwned + Serialize, { @@ -54,7 +64,9 @@ where /// 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, ExecutionError> { + pub async fn execute(self) -> Result> { + let query = self.build()?; + #[derive(Serialize)] struct Record { fields: RT, @@ -63,13 +75,14 @@ where struct RequestBody { records: Vec>, } - 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 + + let base_id = utf8_percent_encode(&query.base_id, NON_ALPHANUMERIC).to_string(); + let table_id = utf8_percent_encode(&query.table_id, NON_ALPHANUMERIC).to_string(); + let http_resp = query .client .post_path(&format!("v0/{base_id}/{table_id}")) .json(&RequestBody { - records: self + records: query .records .into_iter() .map(|rec| Record { fields: rec }) diff --git a/ferrtable/src/errors.rs b/ferrtable/src/errors.rs index e1f0779..62481a7 100644 --- a/ferrtable/src/errors.rs +++ b/ferrtable/src/errors.rs @@ -5,6 +5,9 @@ use thiserror::Error; pub enum ExecutionError { #[error("error making http request to airtable api: {0}")] Reqwest(reqwest::Error), + + #[error("incomplete airtable api request information: {0}")] + Builder(derive_builder::UninitializedFieldError), } impl From for ExecutionError { @@ -12,3 +15,16 @@ impl From for ExecutionError { Self::Reqwest(value) } } + +// In addition to the self-evident purpose of type conversion, this allows our +// type to be specified as the error type for auto-generated `build()` methods, +// by annotating the relevant struct declaration with +// `#[builder(build_fn(error = "crate::errors::ExecutionError"))]`. +impl From for ExecutionError { + fn from(value: derive_builder::UninitializedFieldError) -> Self { + Self::Builder(value) + } +} + +// Custom `Result` type helps to make complex method signatures more concise. +pub type Result = std::result::Result; diff --git a/ferrtable/src/get_record.rs b/ferrtable/src/get_record.rs index 211bf17..d0a360a 100644 --- a/ferrtable/src/get_record.rs +++ b/ferrtable/src/get_record.rs @@ -4,36 +4,50 @@ use derive_builder::Builder; use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode}; use serde::{Serialize, de::DeserializeOwned}; -use crate::{client::Client, errors::ExecutionError, types::AirtableRecord}; +use crate::{ + client::Client, + errors::{ExecutionError, Result}, + types::AirtableRecord, +}; #[derive(Builder, Clone, Debug, Serialize)] -#[builder(pattern = "owned", setter(prefix = "with"))] +#[builder( + build_fn(error = "ExecutionError", private), + pattern = "owned", + setter(prefix = "with") +)] pub struct GetRecordQuery { + #[builder(setter(into))] #[serde(skip)] base_id: String, - #[serde(skip)] #[builder(vis = "pub(crate)")] + #[serde(skip)] client: Client, + #[builder(setter(into))] #[serde(skip)] record_id: String, + #[builder(setter(into))] #[serde(skip)] table_id: String, } -impl GetRecordQuery { - pub async fn fetch_optional(self) -> Result>, ExecutionError> +impl GetRecordQueryBuilder { + pub async fn fetch_optional(self) -> Result>> 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 + let query = self.build()?; + let http_resp = query .client - .get_path(&format!("v0/{base_id}/{table_id}/{record_id}")) + .get_path(&format!( + "v0/{base_id}/{table_id}/{record_id}", + base_id = utf8_percent_encode(&query.base_id, NON_ALPHANUMERIC), + table_id = utf8_percent_encode(&query.table_id, NON_ALPHANUMERIC), + record_id = utf8_percent_encode(&query.record_id, NON_ALPHANUMERIC), + )) .send() .await?; match http_resp.error_for_status() { diff --git a/ferrtable/src/lib.rs b/ferrtable/src/lib.rs index 3c53869..4a0fb87 100644 --- a/ferrtable/src/lib.rs +++ b/ferrtable/src/lib.rs @@ -55,19 +55,17 @@ //! status: Status::InProgress, //! attachments: vec![], //! }]) -//! .with_base_id("***".to_owned()) -//! .with_table_id("***".to_owned()) -//! .build()? +//! .with_base_id("***") +//! .with_table_id("***") //! .execute() //! .await?; //! //! let mut rec_stream = client //! .list_records() -//! .with_base_id("***".to_owned()) -//! .with_table_id("***".to_owned()) -//! .with_filter(Some("{status} = 'Todo' || {status} = 'In Progress'".to_owned())) -//! .build()? -//! .stream_items::(); +//! .with_base_id("***") +//! .with_table_id("***") +//! .with_filter("{status} = 'Todo' || {status} = 'In Progress'") +//! .stream_items::()?; //! //! while let Some(result) = rec_stream.next().await { //! dbg!(result?.fields); diff --git a/ferrtable/src/list_bases.rs b/ferrtable/src/list_bases.rs index 690a15b..312b179 100644 --- a/ferrtable/src/list_bases.rs +++ b/ferrtable/src/list_bases.rs @@ -6,12 +6,16 @@ use serde::{Deserialize, Serialize}; use crate::{ client::Client, - errors::ExecutionError, + errors::{ExecutionError, Result}, pagination::{PaginatedQuery, PaginatedResponse, execute_paginated}, }; #[derive(Builder, Clone, Debug, Serialize)] -#[builder(pattern = "owned", setter(prefix = "with"))] +#[builder( + build_fn(error = "ExecutionError", private), + pattern = "owned", + setter(prefix = "with") +)] pub struct ListBasesQuery { #[serde(skip)] #[builder(vis = "pub(crate)")] @@ -39,9 +43,10 @@ impl PaginatedQuery for ListBasesQuery { } } -impl ListBasesQuery { - pub fn stream_items(self) -> Pin>>> { - execute_paginated::(self) +impl ListBasesQueryBuilder { + pub fn stream_items(self) -> Result>>>> { + self.build() + .map(execute_paginated::) } } diff --git a/ferrtable/src/list_records.rs b/ferrtable/src/list_records.rs index 025911e..8cf9c0b 100644 --- a/ferrtable/src/list_records.rs +++ b/ferrtable/src/list_records.rs @@ -7,25 +7,30 @@ use serde::{Deserialize, Serialize, de::DeserializeOwned}; use crate::{ client::Client, - errors::ExecutionError, + errors::{ExecutionError, Result}, pagination::{PaginatedQuery, PaginatedResponse, execute_paginated}, types::AirtableRecord, }; #[derive(Builder, Clone, Debug, Serialize)] -#[builder(pattern = "owned", setter(prefix = "with"))] +#[builder( + build_fn(error = "ExecutionError", private), + pattern = "owned", + setter(prefix = "with") +)] pub struct ListRecordsQuery { + #[builder(setter(into))] #[serde(skip)] base_id: String, - #[serde(skip)] #[builder(vis = "pub(crate)")] + #[serde(skip)] client: Client, /// Only data for fields whose names or IDs are in this list will be /// included in the result. If you don't need every field, you can use this /// parameter to reduce the amount of data transferred. - #[builder(default)] + #[builder(default, setter(into, strip_option))] fields: Option>, /// A formula used to filter records. The formula will be evaluated for @@ -34,7 +39,7 @@ pub struct ListRecordsQuery { /// /// If combined with the view parameter, only records in that view which /// satisfy the formula will be returned. - #[builder(default)] + #[builder(default, setter(into, strip_option))] // filterByFormula is renamed so that the builder method, that is, // `.with_filter()`, reads more cleanly. #[serde(rename = "filterByFormula")] @@ -43,10 +48,11 @@ pub struct ListRecordsQuery { #[builder(default, private)] offset: Option, + #[builder(default, setter(into, strip_option))] #[serde(rename = "pageSize")] - #[builder(default)] page_size: Option, + #[builder(setter(into))] #[serde(skip)] table_id: String, } @@ -72,14 +78,13 @@ where } } -impl ListRecordsQuery { - pub fn stream_items( - self, - ) -> Pin, ExecutionError>>>> +impl ListRecordsQueryBuilder { + pub fn stream_items(self) -> Result>>>>> where T: Clone + Debug + DeserializeOwned, { - execute_paginated::, ListRecordsResponse>(self) + self.build() + .map(execute_paginated::, ListRecordsResponse>) } }