use std::env;
use std::error;
use std::fmt;
use std::rc::Rc;
use reqwest::{header, Client, Method, Request, StatusCode, Url};
use serde::{Deserialize, Serialize};
const ENDPOINT: &str = "https://api.airtable.com/v0/";
pub struct Airtable {
key: String,
base_id: String,
client: Rc<Client>,
}
impl Airtable {
pub fn new<K, B>(key: K, base_id: B) -> Self
where
K: ToString,
B: ToString,
{
let client = Client::builder().build();
match client {
Ok(c) => Self {
key: key.to_string(),
base_id: base_id.to_string(),
client: Rc::new(c),
},
Err(e) => panic!("creating client failed: {:?}", e),
}
}
pub fn new_from_env() -> Self {
let key = env::var("AIRTABLE_API_KEY").unwrap();
let base_id = env::var("AIRTABLE_BASE_ID").unwrap();
Airtable::new(key, base_id)
}
pub fn get_key(&self) -> &str {
&self.key
}
fn request<B>(
&self,
method: Method,
path: String,
body: B,
query: Option<Vec<(&str, String)>>,
) -> Request
where
B: Serialize,
{
let base = Url::parse(ENDPOINT).unwrap();
let url = base
.join(&(self.base_id.to_string() + "/" + &path))
.unwrap();
let bt = format!("Bearer {}", self.key);
let bearer = header::HeaderValue::from_str(&bt).unwrap();
let mut headers = header::HeaderMap::new();
headers.append(header::AUTHORIZATION, bearer);
headers.append(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
);
let mut rb = self.client.request(method.clone(), url).headers(headers);
match query {
None => (),
Some(val) => {
rb = rb.query(&val);
}
}
if method != Method::GET && method != Method::DELETE {
rb = rb.json(&body);
}
rb.build().unwrap()
}
pub async fn list_records(
&self,
table: &str,
view: &str,
) -> Result<Vec<Record>, APIError> {
let mut request = self.request(
Method::GET,
table.to_string(),
(),
Some(vec![
("pageSize", "100".to_string()),
("view", view.to_string()),
]),
);
let mut resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::OK => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
})
}
};
let mut r: APICall = resp.json().await.unwrap();
let mut records = r.records;
let mut offset = if let Some(o) = r.offset {
o
} else {
"".to_string()
};
while !offset.is_empty() {
request = self.request(
Method::GET,
table.to_string(),
(),
Some(vec![
("pageSize", "100".to_string()),
("view", view.to_string()),
("offset", offset),
]),
);
resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::OK => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
})
}
};
r = resp.json().await.unwrap();
records.append(&mut r.records);
offset = if let Some(o) = r.offset {
o
} else {
"".to_string()
};
}
Ok(records)
}
pub async fn create_records(
&self,
table: &str,
records: Vec<Record>,
) -> Result<Vec<Record>, APIError> {
let request = self.request(
Method::POST,
table.to_string(),
APICall {
records,
offset: None,
typecast: Some(true),
},
None,
);
let resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::OK => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
})
}
};
let r: APICall = resp.json().await.unwrap();
Ok(r.records)
}
pub async fn update_records(
&self,
table: &str,
records: Vec<Record>,
) -> Result<Vec<Record>, APIError> {
let request = self.request(
Method::PATCH,
table.to_string(),
APICall {
records,
offset: None,
typecast: Some(true),
},
None,
);
let resp = self.client.execute(request).await.unwrap();
match resp.status() {
StatusCode::OK => (),
s => {
return Err(APIError {
status_code: s,
body: resp.text().await.unwrap(),
})
}
};
let r: APICall = resp.json().await.unwrap();
Ok(r.records)
}
}
pub struct APIError {
pub status_code: StatusCode,
pub body: String,
}
impl fmt::Display for APIError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"APIError: status code -> {}, body -> {}",
self.status_code.to_string(),
self.body
)
}
}
impl fmt::Debug for APIError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"APIError: status code -> {}, body -> {}",
self.status_code.to_string(),
self.body
)
}
}
impl error::Error for APIError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct APICall {
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<String>,
pub records: Vec<Record>,
#[serde(skip_serializing_if = "Option::is_none")]
pub typecast: Option<bool>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Record {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub fields: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_time: Option<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct User {
pub id: String,
pub email: String,
pub name: String,
}