use crate::{types::*, Error, UserCache};
use reqwest::{
header::{HeaderMap, HeaderValue},
Response,
};
use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value;
use std::sync::{Arc, RwLock};
const DEFAULT_ENDPOINT: &str = "https://zenkit.com/api/v1";
const API_TOKEN_ENV_VAR: &str = "ZENKIT_API_TOKEN";
#[derive(Debug)]
pub struct ApiClient {
client: reqwest::Client,
url_prefix: String,
ratelimit: Option<u32>,
ratelimit_remaining: Option<u32>,
workspaces: RwLock<Vec<Arc<WorkspaceData>>>,
lists: RwLock<Vec<Arc<ListInfo>>>,
}
pub struct ApiConfig {
pub token: String,
pub endpoint: String,
}
impl Default for ApiConfig {
fn default() -> Self {
Self {
endpoint: String::from(DEFAULT_ENDPOINT),
token: std::env::var(API_TOKEN_ENV_VAR).ok().unwrap_or_default(),
}
}
}
fn user_agent_header() -> HeaderValue {
let agent = &format!(
"{} rs {}",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION")
);
HeaderValue::from_str(agent).unwrap_or_else(|_| HeaderValue::from_static("zenkit_rust"))
}
impl ApiClient {
pub(crate) fn new(config: ApiConfig) -> Result<Self, Error> {
use reqwest::header::{CONTENT_TYPE, USER_AGENT};
if config.token.is_empty() {
let env_is_set = match std::env::var(API_TOKEN_ENV_VAR) {
Ok(s) => !s.is_empty(),
Err(_) => false,
};
let msg = match env_is_set {
false => format!(
"Missing API token. Do you need to set the environment variable '{}'?",
API_TOKEN_ENV_VAR
),
true => "Missing API token. The token is needed as a parameter to ApiClient::new()"
.to_string(),
};
return Err(Error::MissingAPIToken(msg));
}
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
headers.insert(USER_AGENT, user_agent_header());
headers.insert(
"Zenkit-API-Key",
HeaderValue::from_str(&config.token)
.map_err(|_| Error::Other("token has non-ascii chars".to_string()))?,
);
let client = reqwest::Client::builder()
.default_headers(headers)
.build()?;
Ok(Self {
client,
url_prefix: config.endpoint,
ratelimit: None,
ratelimit_remaining: None,
workspaces: RwLock::new(Vec::new()),
lists: RwLock::new(Vec::new()),
})
}
pub fn get_rate_limit(&self) -> Option<u32> {
self.ratelimit
}
pub fn get_rate_limit_remaining(&self) -> Option<u32> {
self.ratelimit_remaining
}
async fn json<T: DeserializeOwned>(&self, resp: Response) -> Result<T, Error> {
let status = &resp.status();
let bytes = resp.bytes().await?;
if !status.is_success() {
if let Ok(err_res) = serde_json::from_slice::<ErrorResult>(&bytes) {
return Err(Error::ApiError(status.as_u16(), Some(err_res.error)));
}
return Err(Error::Other(format!(
"Server returned status {}:{}",
status.as_u16(),
String::from_utf8_lossy(bytes.as_ref())
)));
}
match serde_json::from_slice(&bytes) {
Ok(obj) => Ok(obj),
Err(e) => {
eprintln!(
"Error deserializing result {}. data:\n{}",
e.to_string(),
String::from_utf8_lossy(&bytes)
);
Err(e.into())
}
}
}
pub async fn get_users(&self, workspace_id: ID) -> Result<Vec<Arc<User>>, Error> {
let ws_cache_read = self.workspaces.read()?;
let wd = match ws_cache_read
.iter()
.find(|w| w.workspace.id == workspace_id)
{
Some(w) => w,
None => {
return Err(Error::Other(format!(
"get_workspace_users: invalid workspace_id '{}'",
workspace_id
)))
}
};
let wd2 = wd.clone();
drop(ws_cache_read);
Ok(wd2.users().await?)
}
pub async fn get_users_raw(&self, workspace_id: ID) -> Result<Vec<User>, Error> {
let url = format!("{}/workspaces/{}/users", self.url_prefix, workspace_id);
let resp = self.client.get(&url).send().await?;
self.json(resp).await
}
pub async fn find_user<P>(
&self,
workspace_id: ID,
predicate: P,
) -> Result<Option<Arc<User>>, Error>
where
P: Fn(&Arc<User>) -> bool,
{
let ws_cache_read = self.workspaces.read()?;
let wd = match ws_cache_read
.iter()
.find(|w| w.workspace.id == workspace_id)
{
Some(w) => w,
None => {
return Err(Error::Other(format!(
"get_workspace_users: invalid workspace_id '{}'",
workspace_id
)))
}
};
let wd2 = wd.clone();
drop(ws_cache_read);
Ok(wd2.find_user(predicate).await?)
}
pub async fn get_user_id(&self, workspace_id: ID, name: &str) -> Result<Option<ID>, Error> {
let lc_name = name.to_lowercase();
let id = self
.find_user(workspace_id, |u| {
u.display_name.to_lowercase() == lc_name
|| u.full_name.to_lowercase() == lc_name
|| u.uuid == lc_name
})
.await?
.map(|u| u.id);
Ok(id)
}
pub async fn get_user_accesses(&self) -> Result<Vec<Access>, Error> {
let resp = self
.client
.get(&format!("{}/users/me/access", self.url_prefix))
.send()
.await?;
self.json(resp).await
}
pub async fn get_shared_accesses<A: Into<AllId>>(
&self,
user_allid: A,
) -> Result<SharedAccesses, Error> {
let url = format!(
"{}/users/me/matching-access/{}",
self.url_prefix,
user_allid.into()
);
let resp = self.client.get(&url).send().await?;
self.json(resp).await
}
pub async fn get_list_elements<A: Into<AllId>>(
&self,
list_allid: A,
) -> Result<Vec<Element>, Error> {
let url = format!("{}/lists/{}/elements", self.url_prefix, list_allid.into());
let resp = self.client.get(&url).send().await?;
self.json(resp).await
}
pub async fn get_entry<L: Into<AllId>, E: Into<AllId>>(
&self,
list_allid: L,
entry_allid: E,
) -> Result<Entry, Error> {
let url = format!(
"{}/lists/{}/entries/{}",
self.url_prefix,
list_allid.into(),
entry_allid.into()
);
let resp = self.client.get(&url).send().await?;
self.json(resp).await
}
pub async fn get_list_entries<A: Into<AllId>>(
&self,
list_allid: A,
params: &GetEntriesRequest,
) -> Result<Vec<Entry>, Error> {
let url = format!(
"{}/lists/{}/entries/filter",
self.url_prefix,
list_allid.into()
);
let resp = self.client.post(&url).json(¶ms).send().await?;
self.json(resp).await
}
pub async fn get_list_entries_sorted<A: Into<AllId>>(
&self,
list_allid: A,
sort: Option<(&str, SortDirection)>,
limit: usize,
skip: usize,
) -> Result<Vec<Entry>, Error> {
let order_by = if let Some((sort_field, sort_dir)) = sort {
vec![OrderBy {
column: Some(String::from(sort_field)),
direction: sort_dir,
}]
} else {
Vec::new()
};
let q = GetEntriesRequest {
limit,
skip,
order_by,
..Default::default()
};
Ok(self.get_list_entries(list_allid, &q).await?)
}
pub async fn get_list_entries_for_view(
&self,
list_id: ID,
params: &GetEntriesViewRequest,
) -> Result<GetEntriesViewResponse, Error> {
let url = format!("{}/lists/{}/entries/filter/list", self.url_prefix, list_id);
let resp = self.client.post(&url).json(params).send().await?;
self.json(resp).await
}
pub async fn update_checklists<L: Into<AllId>, E: Into<AllId>>(
&self,
list_allid: L,
entry_allid: E,
checklists: Vec<Checklist>,
) -> Result<(), Error> {
let url = format!(
"{}/lists/{}/entries/{}/checklists",
self.url_prefix,
list_allid.into(),
entry_allid.into()
);
let data = UpdateChecklistParam { checklists };
let resp = self.client.put(&url).json(&data).send().await?;
self.json(resp).await
}
pub async fn delete_entry<L: Into<AllId>, E: Into<AllId>>(
&self,
list_allid: L,
entry_allid: E,
) -> Result<DeleteListEntryResponse, Error> {
let url = format!(
"{}/lists/{}/deprecated-entries/{}",
self.url_prefix,
list_allid.into(),
entry_allid.into()
);
let resp = self.client.delete(&url).send().await?;
self.json(resp).await
}
fn have_workspaces(&self) -> Result<bool, Error> {
let ws_cache = self.workspaces.read()?;
Ok(!ws_cache.is_empty())
}
fn get_cached_list(&self, list_allid: &str) -> Result<Arc<ListInfo>, Error> {
let list_cache = self.lists.read()?;
let li = match list_cache.iter().find(|li| li.has_id(list_allid)) {
Some(li) => li.clone(),
None => return Err(Error::Other(format!("Invalid list '{}'", list_allid))),
};
Ok(li)
}
fn get_cached_workspace_allid(&self, ws_id: &str) -> Result<Arc<WorkspaceData>, Error> {
let ws_cache = self.workspaces.read()?;
let wd = match ws_cache.iter().find(|wd| wd.workspace.has_id(ws_id)) {
Some(w) => w.clone(),
None => return Err(Error::Other(format!("Invalid workspace_id '{}'", ws_id))),
};
Ok(wd)
}
fn get_cached_workspace(&self, ws_id: ID) -> Result<Arc<WorkspaceData>, Error> {
let ws_cache_read = self.workspaces.read()?;
let wd = match ws_cache_read.iter().find(|w| w.workspace.id == ws_id) {
Some(w) => w.clone(),
None => return Err(Error::Other(format!("Invalid workspace_id '{}'", ws_id))),
};
Ok(wd)
}
pub async fn get_all_workspaces_and_lists(&self) -> Result<Vec<Arc<Workspace>>, Error> {
if !self.have_workspaces()? {
let resp = self
.client
.get(&format!("{}/users/me/workspacesWithLists", self.url_prefix))
.send()
.await?;
let ws_list: Vec<Workspace> = self.json(resp).await?;
let mut ws_cache_write = self.workspaces.write()?;
ws_cache_write.append(
&mut ws_list
.into_iter()
.map(|w| Arc::new(WorkspaceData::new(w)))
.collect(),
);
}
let ws_cache = self.workspaces.read()?;
Ok(ws_cache.iter().map(|wd| wd.workspace.clone()).collect())
}
pub async fn get_workspace(&self, ws_id: &str) -> Result<Arc<Workspace>, Error> {
if let Ok(wd) = self.get_cached_workspace_allid(ws_id) {
return Ok(wd.workspace.clone());
}
if ws_id.parse::<i64>().is_ok() || crate::util::is_uuid(ws_id) {
let url = format!("{}/workspaces/{}", self.url_prefix, ws_id);
let resp = self.client.get(&url).send().await?;
let ws_data = WorkspaceData::new(self.json(resp).await?);
let mut cache_write = self.workspaces.write()?;
let ws_copy = ws_data.workspace.clone();
cache_write.push(Arc::new(ws_data));
return Ok(ws_copy);
}
for w in self.get_all_workspaces_and_lists().await?.iter() {
if w.has_id(ws_id) {
return Ok(w.clone());
}
}
Err(Error::Other(format!("Workspace '{}' not found", ws_id)))
}
pub async fn get_list_info(
&self,
workspace_id: ID,
list_allid: &'_ str,
) -> Result<Arc<ListInfo>, Error> {
if let Ok(li) = self.get_cached_list(list_allid) {
return Ok(li);
}
let wd = match self.get_cached_workspace(workspace_id) {
Ok(wd) => wd.workspace.clone(),
Err(_) => {
self.get_workspace(&workspace_id.to_string()).await?
}
};
let list = match wd.lists.iter().find(|l| l.has_id(list_allid)) {
Some(list) => list.clone(),
None => {
return Err(Error::Other(format!(
"get_list_info: invalid list '{}' in workspace '{}' ({})",
list_allid, wd.name, workspace_id
)))
}
};
let fields = self.get_list_elements(list.id).await?;
let info = Arc::new(ListInfo::new(list, fields));
let mut list_cache_write = self.lists.write()?;
list_cache_write.push(info.clone());
Ok(info)
}
pub fn clear_workspace_cache(&self) -> Result<(), Error> {
let mut ws_cache_write = self.workspaces.write()?;
ws_cache_write.clear();
Ok(())
}
pub fn clear_list_cache(&self) -> Result<(), Error> {
let mut list_cache_write = self.lists.write()?;
list_cache_write.clear();
Ok(())
}
pub async fn create_entry(&self, list_id: ID, val: Value) -> Result<Entry, Error> {
let url = format!("{}/lists/{}/entries", self.url_prefix, list_id);
let resp = self.client.post(&url).json(&val).send().await?;
self.json(resp).await
}
pub async fn create_webhook(&self, webhook: &NewWebhook) -> Result<Webhook, Error> {
let url = format!("{}/webhooks", self.url_prefix);
let resp = self.client.post(&url).json(&webhook).send().await?;
self.json(resp).await
}
pub async fn delete_webhook(&self, webhook_id: ID) -> Result<Webhook, Error> {
let url = format!("{}/webhooks/{}", self.url_prefix, webhook_id);
let resp = self.client.delete(&url).send().await?;
self.json(resp).await
}
pub async fn get_webhooks(&self) -> Result<Vec<Webhook>, Error> {
let url = format!("{}/users/me/webhooks", self.url_prefix);
let resp = self.client.get(&url).send().await?;
self.json(resp).await
}
pub async fn update_entry(
&self,
list_id: ID,
entry_id: ID,
val: Value,
) -> Result<Entry, Error> {
let url = format!("{}/lists/{}/entries/{}", self.url_prefix, list_id, entry_id);
let resp = self.client.put(&url).json(&val).send().await?;
self.json(resp).await
}
pub async fn create_list_comment(
&self,
list_id: ID,
comment: &NewComment,
) -> Result<Activity, Error> {
let url = format!("{}/users/me/lists/{}/activities", self.url_prefix, list_id);
let resp = self.client.post(&url).json(&comment).send().await?;
self.json(resp).await
}
pub async fn create_entry_comment(
&self,
list_id: ID,
entry_id: ID,
comment: &NewComment,
) -> Result<Activity, Error> {
let url = format!(
"{}/users/me/lists/{}/entries/{}/activities",
self.url_prefix, list_id, entry_id
);
let resp = self.client.post(&url).json(&comment).send().await?;
self.json(resp).await
}
}
#[derive(Serialize, Debug)]
struct UpdateChecklistParam {
checklists: Vec<Checklist>,
}
#[derive(Debug)]
struct WorkspaceData {
workspace: Arc<Workspace>,
user_cache: RwLock<UserCache>,
}
impl WorkspaceData {
fn new(w: Workspace) -> Self {
Self {
workspace: Arc::new(w),
user_cache: Default::default(),
}
}
pub async fn ensure_user_cache(&self, force_reload: bool) -> Result<(), Error> {
let mut write = self.user_cache.write()?;
if write.is_empty() || force_reload {
write.replace_all(
crate::get_api()?
.get_users_raw(self.workspace.id)
.await?
.into_iter()
.map(Arc::new)
.collect(),
);
}
Ok(())
}
pub async fn users(&self) -> Result<Vec<Arc<User>>, Error> {
self.ensure_user_cache(false).await?;
let read = self.user_cache.read()?;
Ok(read.users())
}
pub async fn find_user<P>(&self, predicate: P) -> Result<Option<Arc<User>>, Error>
where
P: Fn(&Arc<User>) -> bool,
{
self.ensure_user_cache(false).await?;
let read = self.user_cache.read()?;
Ok(read.find_user(predicate))
}
}