Skip to content
This repository has been archived by the owner on Dec 23, 2024. It is now read-only.

Commit

Permalink
Pre-launch hijinks
Browse files Browse the repository at this point in the history
  • Loading branch information
anish-dfg committed Oct 27, 2024
1 parent 2e9dd6d commit e3d7f1e
Show file tree
Hide file tree
Showing 33 changed files with 395 additions and 131 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ workspace = { members = [
name = "scipio"
version = "0.1.0"
edition = "2021"
authors = ["Anish Sinha <[email protected]>"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

Expand Down
1 change: 1 addition & 0 deletions migrations/20240722170323_initial.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ language plpgsql;

-- Allowed age ranges for volunteers (from Airtable)
create type age_range as enum(
'17_and_under',
'18-24',
'25-29',
'30-34',
Expand Down
4 changes: 4 additions & 0 deletions scipio-airtable/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ edition = "2021"
anyhow = "1.0.89"
chrono = { version = "0.4.38", features = ["serde"] }
derive_builder = "0.20.2"
derive_more = { version = "1.0.0", features = ["full"] }
dotenvy = "0.15.7"
log = "0.4.22"
reqwest = { version = "0.12.8", default-features = false, features = [
"json",
"rustls-tls",
Expand All @@ -19,6 +21,8 @@ serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"
serde_with = "3.11.0"
tokio = { version = "1.40.0", features = ["full"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"

[dev-dependencies]
rstest = "0.23.0"
Expand Down
3 changes: 0 additions & 3 deletions scipio-airtable/src/base_data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,3 @@ pub mod bases;
pub mod entities;
pub mod records;
pub mod responses;

#[cfg(test)]
mod tests;
64 changes: 59 additions & 5 deletions scipio-airtable/src/base_data/records.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ use std::fmt::Display;

use anyhow::Result;
use derive_builder::Builder;
use derive_more::derive::Display;
use scipio_macros::ToQueryString;
use serde::{Deserialize, Serialize};

use super::responses::ListRecordsResponse;
use super::responses::{GetRecordResponse, ListRecordsResponse};
use crate::Airtable;

/// A struct representing a sort query parameter.
Expand Down Expand Up @@ -58,25 +59,78 @@ pub struct ListRecordsQuery {
pub record_metadata: Option<String>,
}

#[derive(Debug, Serialize, Clone, Display)]
#[serde(rename_all = "snake_case")]
pub enum CellFormat {
#[display("json")]
Json,
#[display("string")]
String,
}

#[derive(Debug, Serialize, Clone, Builder, ToQueryString)]
#[serde(rename_all = "camelCase")]
pub struct GetRecordQuery {
#[builder(setter(into), default)]
pub time_zone: Option<String>,
#[builder(default, setter(into))]
pub user_locale: Option<String>,
#[builder(setter(into), default)]
pub cell_format: Option<CellFormat>,
#[builder(default, setter(into))]
pub return_fields_by_field_id: Option<bool>,
}

impl Airtable {
pub async fn list_records<T>(
&self,
base_id: &str,
table_id: &str,
query: ListRecordsQuery,
query: Option<&ListRecordsQuery>,
) -> Result<ListRecordsResponse<T>>
where
T: for<'de> Deserialize<'de>,
{
let url = format!(
"https://api.airtable.com/v0/{base_id}/{table_id}/{query}",
base_id = base_id,
table_id = table_id,
query = query.to_query_string()
query = query.map(|q| q.to_query_string()).unwrap_or_default()
);

let data = self.http.get(&url).send().await?.json::<ListRecordsResponse<T>>().await?;

Ok(data)
}

pub async fn get_record<T>(
&self,
base_id: &str,
table_id: &str,
record_id: &str,
query: Option<&GetRecordQuery>,
) -> Result<GetRecordResponse<T>>
where
T: for<'de> Deserialize<'de>,
{
let url = format!(
"https://api.airtable.com/v0/{base_id}/{table_id}/{record_id}/{query}",
query = query.map(|q| q.to_query_string()).unwrap_or_default()
);

let data = self.http.get(&url).send().await?.json::<GetRecordResponse<T>>().await?;

Ok(data)
}

pub async fn update_record<T>(
&self,
base_id: &str,
table_id: &str,
record_id: &str,
data: T,
) -> Result<()>
where
T: for<'de> Deserialize<'de>,
{
Ok(())
}
}
2 changes: 2 additions & 0 deletions scipio-airtable/src/base_data/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ pub struct ListRecordsResponse<T> {
pub records: Vec<Record<T>>,
pub offset: Option<String>,
}

pub type GetRecordResponse<T> = Record<T>;
12 changes: 0 additions & 12 deletions scipio-airtable/src/base_data/tests/fixtures.rs

This file was deleted.

3 changes: 0 additions & 3 deletions scipio-airtable/src/base_data/tests/mod.rs

This file was deleted.

51 changes: 0 additions & 51 deletions scipio-airtable/src/base_data/tests/records.rs

This file was deleted.

3 changes: 3 additions & 0 deletions scipio-airtable/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod base_data;
mod retry;
#[cfg(test)]
mod tests;

use anyhow::Result;
use reqwest::header::{self, HeaderMap, HeaderValue};
Expand All @@ -9,6 +11,7 @@ use reqwest_retry::policies::ExponentialBackoff;
use reqwest_retry::RetryTransientMiddleware;
use retry::DefaultRetryStrategy;

#[derive(Clone)]
pub struct Airtable {
http: ClientWithMiddleware,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use rstest::rstest;

use super::fixtures::airtable;
use crate::Airtable;
use super::fixtures::{context, AsyncTestContext};

#[cfg(feature = "integration")]
#[rstest]
#[tokio::test]
pub async fn test_list_bases(airtable: Airtable) {
let bases_response = airtable.list_bases(None).await.unwrap();
pub async fn test_list_bases(context: AsyncTestContext) {
let bases_response = context.airtable.list_bases(None).await.unwrap();
dbg!(&bases_response);
}

Expand Down
53 changes: 53 additions & 0 deletions scipio-airtable/src/tests/fixtures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::{env, thread};

use anyhow::Result;
use rstest::fixture;
use tokio::runtime::Runtime;
use tokio::sync::Mutex;

use crate::Airtable;

type BoxedAsyncFn = Box<dyn Fn() -> Pin<Box<dyn Future<Output = Result<()>> + Send>> + Send + Sync>;

pub struct AsyncTestContext {
pub airtable: Airtable,
pub cleanup: Arc<Mutex<Vec<BoxedAsyncFn>>>,
}

impl Drop for AsyncTestContext {
fn drop(&mut self) {
let cleanup = self.cleanup.clone();
let handle = thread::spawn(move || {
Runtime::new().expect("error creating runtime to handle cleanup").block_on(async move {
let mut error_count = 0;
for cleanup_fn in cleanup.lock().await.iter() {
if let Err(err) = cleanup_fn().await {
error_count += 1;
log::error!("Error during cleanup: {:?}", err);
}
}
if error_count > 0 {
panic!("{} cleanup functions failed", error_count);
}
})
});

handle.join().expect("error joining cleanup thread");
}
}

#[fixture]
pub fn context() -> AsyncTestContext {
dotenvy::dotenv().expect("error loading environment variables");
tracing_subscriber::fmt().with_max_level(tracing::Level::INFO).init();
let api_token =
env::var("TEST_AIRTABLE_API_TOKEN").expect("missing TEST_AIRTABLE_API_TOKEN variable");
let client = Airtable::new(&api_token, 5).expect("error creating Airtable client");

log::info!("Creating async test context");

AsyncTestContext { airtable: client, cleanup: Arc::new(Mutex::new(vec![])) }
}
52 changes: 52 additions & 0 deletions scipio-airtable/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
mod bases;
mod fixtures;
mod records;

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Customer {
#[serde(rename = "Record Id")]
pub record_id: String,
#[serde(rename = "Customer Id")]
pub customer_id: String,
#[serde(rename = "First Name")]
pub first_name: String,
#[serde(rename = "Last Name")]
pub last_name: String,
#[serde(rename = "Company")]
pub company: String,
#[serde(rename = "City")]
pub city: String,
#[serde(rename = "Country")]
pub country: String,
#[serde(rename = "Phone 1")]
pub phone_1: String,
#[serde(rename = "Phone 2")]
pub phone_2: String,
#[serde(rename = "Email")]
pub email: String,
#[serde(rename = "Subscription Date")]
pub subscription_date: String,
#[serde(rename = "Website")]
pub website: String,
}

impl Customer {
pub fn field_names() -> Vec<&'static str> {
vec![
"Record Id",
"Customer Id",
"First Name",
"Last Name",
"Company",
"City",
"Country",
"Phone 1",
"Phone 2",
"Email",
"Subscription Date",
"Website",
]
}
}
Loading

0 comments on commit e3d7f1e

Please sign in to comment.