#[macro_use]
extern crate derive_builder;
use std::collections::HashMap;
use std::str::FromStr;
use std::string::ParseError;
use std::{env, fmt};
use futures::future::try_join_all;
use reqwest::{header, Url};
use serde::{Deserialize, Serialize};
use crate::annotations::{Annotation, InputAnnotation, SearchQuery};
use crate::errors::HypothesisError;
use crate::groups::{Expand, Group, GroupFilters, Member};
use crate::profile::UserProfile;
pub mod annotations;
#[cfg(feature = "cli")]
pub mod cli;
pub mod errors;
pub mod groups;
pub mod profile;
pub const API_URL: &str = "https://api.hypothes.is/api";
fn is_default<T: Default + PartialEq>(t: &T) -> bool {
t == &T::default()
}
pub fn serde_parse<'a, T: Deserialize<'a>>(text: &'a str) -> Result<T, errors::HypothesisError> {
serde_json::from_str::<T>(text).map_err(|e| errors::HypothesisError::APIError {
source: serde_json::from_str::<errors::APIError>(text).unwrap_or_default(),
serde_error: Some(e),
raw_text: text.to_owned(),
})
}
pub struct Hypothesis {
pub username: String,
pub user: UserAccountID,
client: reqwest::Client,
}
impl Hypothesis {
pub fn new(username: &str, developer_key: &str) -> Result<Self, HypothesisError> {
let user = UserAccountID::from_str(username).expect("This should never error");
let mut headers = header::HeaderMap::new();
headers.insert(
header::AUTHORIZATION,
header::HeaderValue::from_str(&format!("Bearer {}", developer_key))
.map_err(HypothesisError::HeaderError)?,
);
headers.insert(
header::ACCEPT,
header::HeaderValue::from_str("application/vnd.hypothesis.v1+json")
.map_err(HypothesisError::HeaderError)?,
);
let client = reqwest::Client::builder()
.default_headers(headers)
.build()
.map_err(HypothesisError::ReqwestError)?;
Ok(Self {
username: username.into(),
user,
client,
})
}
pub fn from_env() -> Result<Self, HypothesisError> {
let username =
env::var("HYPOTHESIS_NAME").map_err(|e| HypothesisError::EnvironmentError {
source: e,
suggestion: "Set the environment variable HYPOTHESIS_NAME to your username".into(),
})?;
let developer_key =
env::var("HYPOTHESIS_KEY").map_err(|e| HypothesisError::EnvironmentError {
source: e,
suggestion: "Set the environment variable HYPOTHESIS_KEY to your personal API key"
.into(),
})?;
Self::new(&username, &developer_key)
}
pub async fn create_annotation(
&self,
annotation: &InputAnnotation,
) -> Result<Annotation, HypothesisError> {
let text = self
.client
.post(&format!("{}/annotations", API_URL))
.json(annotation)
.send()
.await
.map_err(HypothesisError::ReqwestError)?
.text()
.await
.map_err(HypothesisError::ReqwestError)?;
serde_parse::<Annotation>(&text)
}
pub async fn create_annotations(
&self,
annotations: &[InputAnnotation],
) -> Result<Vec<Annotation>, HypothesisError> {
let futures: Vec<_> = annotations
.iter()
.map(|a| self.create_annotation(a))
.collect();
async { try_join_all(futures).await }.await
}
pub async fn update_annotation(
&self,
annotation: &Annotation,
) -> Result<Annotation, HypothesisError> {
let text = self
.client
.patch(&format!("{}/annotations/{}", API_URL, annotation.id))
.json(&annotation)
.send()
.await
.map_err(HypothesisError::ReqwestError)?
.text()
.await
.map_err(HypothesisError::ReqwestError)?;
serde_parse::<Annotation>(&text)
}
pub async fn update_annotations(
&self,
annotations: &[Annotation],
) -> Result<Vec<Annotation>, HypothesisError> {
let futures: Vec<_> = annotations
.iter()
.map(|a| self.update_annotation(a))
.collect();
async { try_join_all(futures).await }.await
}
pub async fn search_annotations(
&self,
query: &SearchQuery,
) -> Result<Vec<Annotation>, HypothesisError> {
let query: HashMap<String, serde_json::Value> = serde_json::from_str(
&serde_json::to_string(&query).map_err(HypothesisError::SerdeError)?,
)
.map_err(HypothesisError::SerdeError)?;
let url = Url::parse_with_params(
&format!("{}/search", API_URL),
query
.into_iter()
.flat_map(|(k, v)| {
if v.is_array() {
v.as_array()
.unwrap()
.iter()
.map(|v| (k.clone(), v.to_string().replace('"', "")))
.collect::<Vec<_>>()
} else {
vec![(k, v.to_string().replace('"', ""))]
}
})
.collect::<Vec<_>>(),
)
.map_err(HypothesisError::URLError)?;
let text = self
.client
.get(url)
.send()
.await
.map_err(HypothesisError::ReqwestError)?
.text()
.await
.map_err(HypothesisError::ReqwestError)?;
#[derive(Deserialize, Debug, Clone, PartialEq)]
struct SearchResult {
rows: Vec<Annotation>,
total: usize,
}
Ok(serde_parse::<SearchResult>(&text)?.rows)
}
pub async fn search_annotations_return_all(
&self,
query: &mut SearchQuery,
) -> Result<Vec<Annotation>, HypothesisError> {
let mut annotations = Vec::new();
loop {
let next = self.search_annotations(query).await?;
if next.is_empty() {
break;
}
query.search_after = next[next.len() - 1].updated.to_rfc3339();
annotations.extend_from_slice(&next);
}
Ok(annotations)
}
pub async fn fetch_annotation(&self, id: &str) -> Result<Annotation, HypothesisError> {
let text = self
.client
.get(&format!("{}/annotations/{}", API_URL, id))
.send()
.await
.map_err(HypothesisError::ReqwestError)?
.text()
.await
.map_err(HypothesisError::ReqwestError)?;
serde_parse::<Annotation>(&text)
}
pub async fn fetch_annotations(
&self,
ids: &[String],
) -> Result<Vec<Annotation>, HypothesisError> {
let futures: Vec<_> = ids.iter().map(|id| self.fetch_annotation(id)).collect();
async { try_join_all(futures).await }.await
}
pub async fn delete_annotation(&self, id: &str) -> Result<bool, HypothesisError> {
let text = self
.client
.delete(&format!("{}/annotations/{}", API_URL, id))
.send()
.await
.map_err(HypothesisError::ReqwestError)?
.text()
.await
.map_err(HypothesisError::ReqwestError)?;
#[derive(Deserialize, Debug, Clone, PartialEq)]
struct DeletionResult {
id: String,
deleted: bool,
}
Ok(serde_parse::<DeletionResult>(&text)?.deleted)
}
pub async fn delete_annotations(&self, ids: &[String]) -> Result<Vec<bool>, HypothesisError> {
let futures: Vec<_> = ids.iter().map(|id| self.delete_annotation(id)).collect();
async { try_join_all(futures).await }.await
}
pub async fn flag_annotation(&self, id: &str) -> Result<(), HypothesisError> {
let text = self
.client
.put(&format!("{}/annotations/{}/flag", API_URL, id))
.send()
.await
.map_err(HypothesisError::ReqwestError)?
.text()
.await
.map_err(HypothesisError::ReqwestError)?;
let error = serde_json::from_str::<errors::APIError>(&text);
if let Ok(error) = error {
Err(HypothesisError::APIError {
source: error,
raw_text: text,
serde_error: None,
})
} else {
Ok(())
}
}
pub async fn hide_annotation(&self, id: &str) -> Result<(), HypothesisError> {
let text = self
.client
.put(&format!("{}/annotations/{}/hide", API_URL, id))
.send()
.await
.map_err(HypothesisError::ReqwestError)?
.text()
.await
.map_err(HypothesisError::ReqwestError)?;
let error = serde_json::from_str::<errors::APIError>(&text);
if let Ok(error) = error {
Err(HypothesisError::APIError {
source: error,
raw_text: text,
serde_error: None,
})
} else {
Ok(())
}
}
pub async fn show_annotation(&self, id: &str) -> Result<(), HypothesisError> {
let text = self
.client
.delete(&format!("{}/annotations/{}/hide", API_URL, id))
.send()
.await
.map_err(HypothesisError::ReqwestError)?
.text()
.await
.map_err(HypothesisError::ReqwestError)?;
let error = serde_json::from_str::<errors::APIError>(&text);
if let Ok(error) = error {
Err(HypothesisError::APIError {
source: error,
raw_text: text,
serde_error: None,
})
} else {
Ok(())
}
}
pub async fn get_groups(&self, query: &GroupFilters) -> Result<Vec<Group>, HypothesisError> {
let query: HashMap<String, serde_json::Value> = serde_json::from_str(
&serde_json::to_string(&query).map_err(HypothesisError::SerdeError)?,
)
.map_err(HypothesisError::SerdeError)?;
let url = Url::parse_with_params(
&format!("{}/groups", API_URL),
query
.into_iter()
.map(|(k, v)| (k, v.to_string().replace('"', "")))
.collect::<Vec<_>>(),
)
.map_err(HypothesisError::URLError)?;
let text = self
.client
.get(url)
.send()
.await
.map_err(HypothesisError::ReqwestError)?
.text()
.await
.map_err(HypothesisError::ReqwestError)?;
serde_parse::<Vec<Group>>(&text)
}
pub async fn create_group(
&self,
name: &str,
description: Option<&str>,
) -> Result<Group, HypothesisError> {
let mut params = HashMap::new();
params.insert("name", name);
if let Some(description) = description {
params.insert("description", description);
}
let text = self
.client
.post(&format!("{}/groups", API_URL))
.json(¶ms)
.send()
.await
.map_err(HypothesisError::ReqwestError)?
.text()
.await
.map_err(HypothesisError::ReqwestError)?;
serde_parse::<Group>(&text)
}
pub async fn create_groups(
&self,
names: &[String],
descriptions: &[Option<String>],
) -> Result<Vec<Group>, HypothesisError> {
let futures: Vec<_> = names
.iter()
.zip(descriptions.iter())
.map(|(name, description)| self.create_group(name, description.as_deref()))
.collect();
async { try_join_all(futures).await }.await
}
pub async fn fetch_group(
&self,
id: &str,
expand: Vec<Expand>,
) -> Result<Group, HypothesisError> {
let params: HashMap<&str, Vec<String>> = if !expand.is_empty() {
vec![(
"expand",
expand
.into_iter()
.map(|e| serde_json::to_string(&e))
.collect::<Result<_, _>>()
.map_err(HypothesisError::SerdeError)?,
)]
.into_iter()
.collect()
} else {
HashMap::new()
};
let text = self
.client
.get(&format!("{}/groups/{}", API_URL, id))
.json(¶ms)
.send()
.await
.map_err(HypothesisError::ReqwestError)?
.text()
.await
.map_err(HypothesisError::ReqwestError)?;
serde_parse::<Group>(&text)
}
pub async fn fetch_groups(
&self,
ids: &[String],
expands: Vec<Vec<Expand>>,
) -> Result<Vec<Group>, HypothesisError> {
let futures: Vec<_> = ids
.iter()
.zip(expands.into_iter())
.map(|(id, expand)| self.fetch_group(id, expand))
.collect();
async { try_join_all(futures).await }.await
}
pub async fn update_group(
&self,
id: &str,
name: Option<&str>,
description: Option<&str>,
) -> Result<Group, HypothesisError> {
let mut params = HashMap::new();
if let Some(name) = name {
params.insert("name", name);
}
if let Some(description) = description {
params.insert("description", description);
}
let text = self
.client
.patch(&format!("{}/groups/{}", API_URL, id))
.json(¶ms)
.send()
.await
.map_err(HypothesisError::ReqwestError)?
.text()
.await
.map_err(HypothesisError::ReqwestError)?;
serde_parse::<Group>(&text)
}
pub async fn update_groups(
&self,
ids: &[String],
names: &[Option<String>],
descriptions: &[Option<String>],
) -> Result<Vec<Group>, HypothesisError> {
let futures: Vec<_> = ids
.iter()
.zip(names.iter())
.zip(descriptions.iter())
.map(|((id, name), description)| {
self.update_group(id, name.as_deref(), description.as_deref())
})
.collect();
async { try_join_all(futures).await }.await
}
pub async fn get_group_members(&self, id: &str) -> Result<Vec<Member>, HypothesisError> {
let text = self
.client
.get(&format!("{}/groups/{}/members", API_URL, id))
.send()
.await
.map_err(HypothesisError::ReqwestError)?
.text()
.await
.map_err(HypothesisError::ReqwestError)?;
serde_parse::<Vec<Member>>(&text)
}
pub async fn leave_group(&self, id: &str) -> Result<(), HypothesisError> {
let text = self
.client
.delete(&format!("{}/groups/{}/members/me", API_URL, id))
.send()
.await
.map_err(HypothesisError::ReqwestError)?
.text()
.await
.map_err(HypothesisError::ReqwestError)?;
let error = serde_json::from_str::<errors::APIError>(&text);
if let Ok(error) = error {
Err(HypothesisError::APIError {
source: error,
raw_text: text,
serde_error: None,
})
} else {
Ok(())
}
}
pub async fn fetch_user_profile(&self) -> Result<UserProfile, HypothesisError> {
let text = self
.client
.get(&format!("{}/profile", API_URL))
.send()
.await
.map_err(HypothesisError::ReqwestError)?
.text()
.await
.map_err(HypothesisError::ReqwestError)?;
serde_parse::<UserProfile>(&text)
}
pub async fn fetch_user_groups(&self) -> Result<Vec<Group>, HypothesisError> {
let text = self
.client
.get(&format!("{}/profile/groups", API_URL))
.send()
.await
.map_err(HypothesisError::ReqwestError)?
.text()
.await
.map_err(HypothesisError::ReqwestError)?;
serde_parse::<Vec<Group>>(&text)
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
pub struct UserAccountID(pub String);
impl UserAccountID {
pub fn to_username(&self) -> String {
if self.0.len() < 5 {
String::new()
} else {
self.0
.to_owned()
.split_off(5)
.split('@')
.next()
.unwrap_or("")
.to_owned()
}
}
pub fn to_user_id(&self) -> String {
self.0.to_owned()
}
}
impl FromStr for UserAccountID {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(format!("acct:{}@hypothes.is", s)))
}
}
impl fmt::Display for UserAccountID {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&UserAccountID> for UserAccountID {
#[inline]
fn from(a: &UserAccountID) -> UserAccountID {
UserAccountID(a.0.to_owned())
}
}