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

Commit

Permalink
Massive refactor to significantly improve the code structure, remove …
Browse files Browse the repository at this point in the history
…unnecessary modules, and provide cleaner interfaces
  • Loading branch information
anish-dfg committed Oct 13, 2024
1 parent f7d507b commit d05182c
Show file tree
Hide file tree
Showing 62 changed files with 2,448 additions and 1,987 deletions.
465 changes: 368 additions & 97 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
workspace = { members = ["scipio-macros"] }
workspace = { members = [
"scipio-airtable",
"scipio-macros",
"scipio-sendgrid",
"scipio-workspace",
] }
[package]
name = "scipio"
version = "0.1.0"
Expand Down Expand Up @@ -58,6 +63,9 @@ tokio = { version = "1.38.1", features = ["full"] }
tracing-subscriber = "0.3.18"
uuid = { version = "1.10.0", features = ["v4", "serde"] }
scipio-macros = { path = "scipio-macros" }
scipio-workspace = { path = "scipio-workspace" }
scipio-airtable = { path = "scipio-airtable" }
scipio-sendgrid = { path = "scipio-sendgrid" }
serde_urlencoded = "0.7.1"
tracing = "0.1.40"
tower-http = { version = "0.5.2", features = ["full"] }
Expand Down
21 changes: 0 additions & 21 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,3 @@ services:
hostname: pgadmin
depends_on:
- postgres
redis:
hostname: redis
container_name: pantheon-redis
image: redis
ports:
- "6379:6379"
redis-insight:
hostname: redis-ui
container_name: pantheon-redis-ui
image: redislabs/redisinsight
ports:
- "8001:5540"
depends_on:
- redis
nats:
container_name: pantheon-nats
image: "nats:2.10.19"
ports:
- "8222:8222"
- "4222:4222"
hostname: nats-server
26 changes: 26 additions & 0 deletions scipio-airtable/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "scipio-airtable"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.89"
chrono = { version = "0.4.38", features = ["serde"] }
derive_builder = "0.20.2"
dotenvy = "0.15.7"
reqwest = { version = "0.12.8", features = ["json", "rustls-tls"] }
reqwest-middleware = "0.3.3"
reqwest-retry = "0.6.1"
scipio-macros = { path = "../scipio-macros" }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"
serde_with = "3.11.0"
tokio = { version = "1.40.0", features = ["full"] }

[dev-dependencies]
rstest = "0.23.0"


[features]
default = []
integration = []
30 changes: 30 additions & 0 deletions scipio-airtable/src/base_data/bases.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use anyhow::Result;

use super::responses::{ListBasesResponse, SchemaResponse};
use crate::Airtable;

impl Airtable {
pub async fn list_bases(&self, offset: Option<String>) -> Result<ListBasesResponse> {
let mut url = "https://api.airtable.com/v0/meta/bases".to_owned();
if let Some(offset) = offset {
url.push_str(&format!("?offset={}", offset));
}
let data = self.http.get(&url).send().await?.json::<ListBasesResponse>().await?;
Ok(data)
}

pub async fn get_base_schema(
&self,
base_id: &str,
include: Vec<String>,
) -> Result<SchemaResponse> {
let query =
include.iter().map(|v| format!("include={}", v)).collect::<Vec<String>>().join("&");

let url = format!("https://api.airtable.com/v0/meta/bases/{base_id}/tables?{query}");

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

Ok(data)
}
}
121 changes: 121 additions & 0 deletions scipio-airtable/src/base_data/entities.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;

/// The permission level for a base.
///
/// More information about this definition can be found
/// [here](https://airtable.com/developers/web/api/list-bases). We have an `Other` variant to
/// ensure forward compatibility with new permission levels if Airtable introduces them.
#[derive(Debug, Serialize, Clone, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum PermissionLevel {
None,
Read,
Comment,
Edit,
Create,
Other(String),
}

impl<'de> Deserialize<'de> for PermissionLevel {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;

match s.as_ref() {
"none" => Ok(PermissionLevel::None),
"read" => Ok(PermissionLevel::Read),
"comment" => Ok(PermissionLevel::Comment),
"edit" => Ok(PermissionLevel::Edit),
"create" => Ok(PermissionLevel::Create),
_ => Ok(PermissionLevel::Other(s)),
}
}
}

/// A base in Airtable.
///
/// * `id`: The ID of the base
/// * `name`: The name of the base
/// * `permission_level`: The permission level the API token used to fetch the base has on the
/// base.
///
/// More information about this definition can be found
/// [here](https://airtable.com/developers/web/api/list-bases)
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Base {
pub id: String,
pub name: String,
#[serde(rename = "permissionLevel")]
pub permission_level: PermissionLevel,
}

/// A field in an Airtable table.
///
/// * `id`: The ID of the field
/// * `_type`: The type of the field
/// * `name`: The name of the field
/// * `description`: The description of the field
/// * `options`: Custom options for the field
#[serde_with::skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Field {
pub id: String,
#[serde(rename = "type")]
pub _type: Option<String>,
pub name: String,
pub description: Option<String>,
pub options: Option<Value>,
}

/// A view in an Airtable table.
///
/// * `id`: The ID of the view
/// * `_type`: The type of the view
/// * `name`: The name of the view
/// * `visible_field_ids`: The IDs of the fields that are visible in the view
#[serde_with::skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct View {
pub id: String,
#[serde(rename = "type")]
pub _type: String,
pub name: String,
pub visible_field_ids: Option<Vec<String>>,
}

/// A table in an Airtable base.
///
/// * `id`: The ID of the table
/// * `primary_field_id`: The ID of the primary field in the table
/// * `name`: The name of the table
/// * `description`: The description of the table
/// * `fields`: The fields in the table
/// * `views`: The views in the table
#[serde_with::skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Table {
pub id: String,
#[serde(rename = "primaryFieldId")]
pub primary_field_id: String,
pub name: String,
pub description: Option<String>,
// #[serde(skip_serializing)]
pub fields: Vec<Field>,
pub views: Vec<View>,
}

/// A record in an Airtable table.
///
/// * `id`: The ID of the record.
/// * `fields`: The fields of the record.
/// * `created_time`: When the record was created.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Record<T> {
pub id: String,
pub fields: T,
pub created_time: String,
}
7 changes: 7 additions & 0 deletions scipio-airtable/src/base_data/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pub mod bases;
pub mod entities;
pub mod records;
pub mod responses;

#[cfg(test)]
mod tests;
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
//! This module defines the `AirtableRecordsClient` trait and implements it for `DfgAirtableClient`.
use std::fmt::Display;

use anyhow::Result;
use async_trait::async_trait;
use derive_builder::Builder;
use scipio_macros::ToQueryString;
use serde::Serialize;
use serde_json::Value;

use crate::services::airtable::base_data::records::responses::ListRecordsResponse;
use crate::services::airtable::DfgAirtableClient;
use serde::{Deserialize, Serialize};

pub mod responses;
use super::responses::ListRecordsResponse;
use crate::Airtable;

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

/// A trait to call the Airtable records API.
///
/// This trait is required to auto-impl `AirtableClient`
#[async_trait]
pub trait AirtableRecordsClient {
/// List records from a table.
///
/// * `base_id`: The base ID that the table is in
/// * `table_id_or_name`: The ID or name of the table to list records from
/// * `query`: Query parameters for the request
async fn list_records(
&self,
base_id: &str,
table_id_or_name: &str,
query: Option<ListRecordsQuery>,
) -> Result<ListRecordsResponse<Value>>;
}

#[async_trait]
impl AirtableRecordsClient for DfgAirtableClient {
async fn list_records(
impl Airtable {
pub async fn list_records<T>(
&self,
base_id: &str,
table_id_or_name: &str,
query: Option<ListRecordsQuery>,
) -> Result<ListRecordsResponse<Value>> {
let mut url = format!("https://api.airtable.com/v0/{base_id}/{table_id_or_name}");
if let Some(ref qs) = query {
url.push_str(&qs.to_query_string());
}
table_id: &str,
query: 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()
);

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

Ok(data)
}
Expand Down
29 changes: 29 additions & 0 deletions scipio-airtable/src/base_data/responses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use serde::{Deserialize, Serialize};

use super::entities::{Base, Record, Table};

/// Base response from the Airtable API.
///
/// * `offset`: The offset to start listing bases from if we need to fetch more.
/// * `bases`: The bases returned from the API.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListBasesResponse {
pub offset: Option<String>,
pub bases: Vec<Base>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SchemaResponse {
pub tables: Vec<Table>,
}

/// Response from the Airtable API for listing records.
///
/// * `records`: The records returned from the API.
/// * `offset`: The offset to start listing records from if we need to fetch more. (a pagination
/// token).
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ListRecordsResponse<T> {
pub records: Vec<Record<T>>,
pub offset: Option<String>,
}
23 changes: 23 additions & 0 deletions scipio-airtable/src/base_data/tests/bases.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use rstest::rstest;

use super::fixtures::airtable;
use crate::Airtable;

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

// #[cfg(feature = "integration")]
// #[rstest]
// #[tokio::test]
// pub async fn test_get_base_schema(airtable: Airtable) {
// let bases_response = airtable.list_bases(None).await.unwrap();
// let base_id =
// bases_response.bases.first().expect("there should be at least one base").id.as_str();
// let schema_response = airtable.get_base_schema(base_id, vec![]).await.unwrap();
// dbg!(&schema_response);
// }
12 changes: 12 additions & 0 deletions scipio-airtable/src/base_data/tests/fixtures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use std::env;

use rstest::fixture;

use crate::Airtable;

#[fixture]
pub fn airtable() -> Airtable {
dotenvy::dotenv().expect("error loading environment variables");
let api_token = env::var("AIRTABLE_API_TOKEN").expect("missing AIRTABLE_API_TOKEN variable");
Airtable::new(&api_token, 5).expect("error creating Airtable client")
}
3 changes: 3 additions & 0 deletions scipio-airtable/src/base_data/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod bases;
mod fixtures;
mod records;
Loading

0 comments on commit d05182c

Please sign in to comment.