hypothesis_rs/
lib.rs

1//! [![Crates.io](https://img.shields.io/crates/v/hypothesis.svg)](https://crates.io/crates/hypothesis)
2//! [![Docs.rs](https://docs.rs/hypothesis/badge.svg)](https://docs.rs/hypothesis)
3//! [![CI](https://github.com/out-of-cheese-error/rust-hypothesis/workflows/Continuous%20Integration/badge.svg)](https://github.com/out-of-cheese-error/rust-hypothesis/actions)
4//! [![GitHub release](https://img.shields.io/github/release/out-of-cheese-error/rust-hypothesis.svg)](https://GitHub.com/out-of-cheese-error/rust-hypothesis/releases/)
5//! [![dependency status](https://deps.rs/repo/github/out-of-cheese-error/rust-hypothesis/status.svg)](https://deps.rs/repo/github/out-of-cheese-error/rust-hypothesis)
6//!
7//! # A Rust API for [Hypothesis](https://web.hypothes.is/)
8//!
9//! ## Description
10//! A lightweight wrapper and CLI for the [Hypothesis Web API v1.0.0](https://h.readthedocs.io/en/latest/api-reference/v1/).
11//! It includes all APIKey authorized endpoints related to
12//! * annotations (create / update / delete / search / fetch / flag),
13//! * groups (create / update / list / fetch / leave / members)
14//! * profile (user information / groups)
15//!
16//! ## Installation and Usage
17//! ### Authorization
18//! You'll need a [Hypothesis](https://hypothes.is) account, and a personal API token obtained as described [here](https://h.readthedocs.io/en/latest/api/authorization/).
19//! Set the environment variables `$HYPOTHESIS_NAME` and `$HYPOTHESIS_KEY` to your username and the developer API key respectively.
20//!
21//! ### As a command-line utility:
22//! ```bash
23//! cargo install hypothesis
24//! ```
25//! Run `hypothesis --help` to see subcommands and options.
26//! NOTE: the CLI doesn't currently have all the capabilities of the Rust crate, specifically bulk actions and updating dates are not supported.
27//!
28//! Generate shell completions:
29//! ```bash
30//! hypothesis complete zsh > .oh-my-zsh/completions/_hypothesis
31//! exec zsh
32//! ```
33//!
34//! ### As a Rust crate
35//! Add to your Cargo.toml:
36//! ```toml
37//! [dependencies]
38//! hypothesis = {version = "0.4.0", default-features = false}
39//! # For a tokio runtime:
40//! tokio = { version = "0.2", features = ["macros"] }
41//! ```
42//!
43//! #### Examples
44//! ```rust no_run
45//! use hypothesis::Hypothesis;
46//! use hypothesis::annotations::{InputAnnotation, Target, Selector};
47//!
48//! #[tokio::main]
49//! async fn main() -> Result<(), hypothesis::errors::HypothesisError> {
50//!     let api = Hypothesis::from_env()?;
51//!     let new_annotation = InputAnnotation::builder()
52//!             .uri("https://www.example.com")
53//!             .text("this is a comment")
54//!             .target(Target::builder()
55//!                .source("https://www.example.com")
56//!                .selector(vec![Selector::new_quote("exact text in website to highlight",
57//!                                                   "prefix of text",
58//!                                                   "suffix of text")])
59//!                .build()?)
60//!            .tags(vec!["tag1".to_string(), "tag2".to_string()])
61//!            .build()?;
62//!     api.create_annotation(&new_annotation).await?;
63//!     Ok(())
64//! }
65//! ```
66//! See the documentation of the API struct ([`Hypothesis`](https://docs.rs/crate/hypothesis/struct.Hypothesis.html)) for a list of possible queries.
67//! Use bulk functions to perform multiple actions - e.g. `api.fetch_annotations` instead of a loop around `api.fetch_annotation`.
68//!
69//! Check the [documentation](https://docs.rs/crate/hypothesis) for more usage examples.
70//!
71//! ### Changelog
72//! See the [CHANGELOG](CHANGELOG.md)
73//!
74//! ### Contributing
75//! Make sure you have a .env file (added to .gitignore) in the repo root with HYPOTHESIS_NAME, HYPOTHESIS_KEY, and TEST_GROUP_ID
76//!
77//! ### Caveats / Todo:
78//! - Only supports APIKey authorization and hypothes.is authority (i.e. single users).
79//! - `Target.selector.RangeSelector` doesn't seem to follow [W3C standards](https://www.w3.org/TR/annotation-model/#range-selector). It's just a hashmap for now.
80//! - `Annotation` hypermedia links are stored as a hashmap, b/c I don't know all the possible values.
81//! - Need to figure out how `Document` works to properly document it (hah).
82//! - Can't delete a group after making it, can leave it though (maybe it's the same thing?)
83//! - No idea what `UserProfile.preferences` and `UserProfile.features` mean.
84//! - CLI just dumps output as JSON, this is fine right? Fancier CLIs can build on top of this (or use the crate directly)
85#[macro_use]
86extern crate derive_builder;
87
88use std::collections::HashMap;
89use std::str::FromStr;
90use std::string::ParseError;
91use std::{env, fmt};
92
93use futures::future::try_join_all;
94use reqwest::{header, Url};
95use serde::{Deserialize, Serialize};
96use time::format_description::well_known::Rfc3339;
97
98use crate::annotations::{Annotation, InputAnnotation, SearchQuery};
99use crate::errors::HypothesisError;
100use crate::groups::{Expand, Group, GroupFilters, Member};
101use crate::profile::UserProfile;
102
103pub mod annotations;
104#[cfg(feature = "cli")]
105pub mod cli;
106pub mod errors;
107pub mod groups;
108pub mod profile;
109
110/// Hypothesis API URL
111pub const API_URL: &str = "https://api.hypothes.is/api";
112
113/// checks if a variable is the default value of its type
114fn is_default<T: Default + PartialEq>(t: &T) -> bool {
115    t == &T::default()
116}
117
118pub fn serde_parse<'a, T: Deserialize<'a>>(text: &'a str) -> Result<T, errors::HypothesisError> {
119    serde_json::from_str::<T>(text).map_err(|e| errors::HypothesisError::APIError {
120        source: serde_json::from_str::<errors::APIError>(text).unwrap_or_default(),
121        serde_error: Some(e),
122        raw_text: text.to_owned(),
123    })
124}
125
126/// Hypothesis API client
127pub struct Hypothesis {
128    /// Authenticated user
129    pub username: String,
130    /// "acct:{username}@hypothes.is"
131    pub user: UserAccountID,
132    /// authorized reqwest async client
133    client: reqwest::Client,
134}
135
136impl Hypothesis {
137    /// Make a new Hypothesis client with your username and developer key
138    /// (see [here](https://h.readthedocs.io/en/latest/api/authorization/) on how to get one)
139    /// # Example
140    /// ```
141    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
142    /// use hypothesis::Hypothesis;
143    /// #     dotenv::dotenv()?;
144    /// #     let username = dotenv::var("HYPOTHESIS_NAME")?;
145    /// #     let developer_key = dotenv::var("HYPOTHESIS_KEY")?;
146    /// let api = Hypothesis::new(&username, &developer_key)?;
147    /// #     Ok(())
148    /// # }
149    /// ```
150    pub fn new(username: &str, developer_key: &str) -> Result<Self, HypothesisError> {
151        let user = UserAccountID::from_str(username).expect("This should never error");
152        let mut headers = header::HeaderMap::new();
153        headers.insert(
154            header::AUTHORIZATION,
155            header::HeaderValue::from_str(&format!("Bearer {}", developer_key))
156                .map_err(HypothesisError::HeaderError)?,
157        );
158        headers.insert(
159            header::ACCEPT,
160            header::HeaderValue::from_str("application/vnd.hypothesis.v1+json")
161                .map_err(HypothesisError::HeaderError)?,
162        );
163        let client = reqwest::Client::builder()
164            .default_headers(headers)
165            .build()
166            .map_err(HypothesisError::ReqwestError)?;
167        Ok(Self {
168            username: username.into(),
169            user,
170            client,
171        })
172    }
173
174    /// Make a new Hypothesis client from environment variables.
175    /// Username from `$HYPOTHESIS_NAME`,
176    /// Developer key from `$HYPOTHESIS_KEY`
177    /// (see [here](https://h.readthedocs.io/en/latest/api/authorization/) on how to get one)
178    /// # Example
179    /// ```
180    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
181    /// #    use std::env;
182    /// #    dotenv::dotenv()?;
183    /// #    let username = dotenv::var("HYPOTHESIS_NAME")?;
184    /// #    let developer_key = dotenv::var("HYPOTHESIS_KEY")?;
185    /// #    env::set_var("HYPOTHESIS_NAME", username);
186    /// #    env::set_var("HYPOTHESIS_KEY", developer_key);
187    /// use hypothesis::Hypothesis;
188    /// let api = Hypothesis::from_env()?;
189    /// #     Ok(())
190    /// # }
191    /// ```
192    pub fn from_env() -> Result<Self, HypothesisError> {
193        let username =
194            env::var("HYPOTHESIS_NAME").map_err(|e| HypothesisError::EnvironmentError {
195                source: e,
196                suggestion: "Set the environment variable HYPOTHESIS_NAME to your username".into(),
197            })?;
198        let developer_key =
199            env::var("HYPOTHESIS_KEY").map_err(|e| HypothesisError::EnvironmentError {
200                source: e,
201                suggestion: "Set the environment variable HYPOTHESIS_KEY to your personal API key"
202                    .into(),
203            })?;
204        Self::new(&username, &developer_key)
205    }
206
207    /// Create a new annotation
208    ///
209    /// Posts a new annotation object to Hypothesis.
210    /// Returns an [`Annotation`](annotations/struct.Annotation.html) as output.
211    /// See [`InputAnnotation`](annotations/struct.InputAnnotation.html) for examples on what you can add to an annotation.
212    ///
213    /// # Example
214    /// ```
215    /// # #[tokio::main]
216    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
217    /// use hypothesis::Hypothesis;
218    /// use hypothesis::annotations::InputAnnotation;
219    /// #     dotenv::dotenv()?;
220    /// #     let username = dotenv::var("HYPOTHESIS_NAME")?;
221    /// #     let developer_key = dotenv::var("HYPOTHESIS_KEY")?;
222    /// #     let group_id = dotenv::var("TEST_GROUP_ID").unwrap_or("__world__".into());
223    ///
224    /// let api = Hypothesis::new(&username, &developer_key)?;
225    /// let annotation = api.create_annotation(&InputAnnotation::builder()
226    ///                     .text("string")
227    ///                     .uri("http://example.com")
228    ///                     .group(&group_id)
229    ///                     .build()?).await?;
230    /// assert_eq!(&annotation.text, "string");
231    /// #    api.delete_annotation(&annotation.id).await?;
232    /// #    Ok(())
233    /// # }
234    /// ```
235    pub async fn create_annotation(
236        &self,
237        annotation: &InputAnnotation,
238    ) -> Result<Annotation, HypothesisError> {
239        let text = self
240            .client
241            .post(&format!("{}/annotations", API_URL))
242            .json(annotation)
243            .send()
244            .await
245            .map_err(HypothesisError::ReqwestError)?
246            .text()
247            .await
248            .map_err(HypothesisError::ReqwestError)?;
249        serde_parse::<Annotation>(&text)
250    }
251
252    /// Create many new annotations
253    ///
254    /// Posts multiple new annotation objects asynchronously to Hypothesis.
255    /// Returns [`Annotation`](annotations/struct.Annotation.html)s as output.
256    /// See [`InputAnnotation`'s](annotations/struct.InputAnnotation.html) docs for examples on what
257    /// you can add to an annotation.
258    ///
259    /// # Example
260    /// ```
261    /// # #[tokio::main]
262    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
263    /// # use hypothesis::Hypothesis;
264    /// # use hypothesis::annotations::InputAnnotation;
265    /// #     dotenv::dotenv()?;
266    /// #     let username = dotenv::var("HYPOTHESIS_NAME")?;
267    /// #     let developer_key = dotenv::var("HYPOTHESIS_KEY")?;
268    /// #     let group_id = dotenv::var("TEST_GROUP_ID").unwrap_or("__world__".into());
269    /// let api = Hypothesis::new(&username, &developer_key)?;
270    /// let input_annotations = vec![
271    ///     InputAnnotation::builder()
272    ///         .text("first")
273    ///         .uri("http://example.com")
274    ///         .group(&group_id)
275    ///         .build()?,
276    ///     InputAnnotation::builder()
277    ///         .text("second")
278    ///         .uri("http://example.com")
279    ///         .group(&group_id)   
280    ///         .build()?
281    /// ];
282    /// let annotations = api.create_annotations(&input_annotations).await?;
283    /// assert_eq!(&annotations[0].text, "first");
284    /// assert_eq!(&annotations[1].text, "second");
285    /// #    api.delete_annotations(&annotations.into_iter().map(|a| a.id).collect::<Vec<_>>()).await?;
286    /// #    Ok(())
287    /// # }
288    /// ```
289    pub async fn create_annotations(
290        &self,
291        annotations: &[InputAnnotation],
292    ) -> Result<Vec<Annotation>, HypothesisError> {
293        let futures: Vec<_> = annotations
294            .iter()
295            .map(|a| self.create_annotation(a))
296            .collect();
297        async { try_join_all(futures).await }.await
298    }
299
300    /// Update an existing annotation
301    ///
302    /// Change any field in an existing annotation. Returns the modified [`Annotation`](annotations/struct.Annotation.html)
303    ///
304    /// # Example
305    /// ```
306    /// # #[tokio::main]
307    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
308    /// use hypothesis::Hypothesis;
309    /// use hypothesis::annotations::InputAnnotation;
310    /// #     dotenv::dotenv()?;
311    /// #     let username = dotenv::var("HYPOTHESIS_NAME")?;
312    /// #     let developer_key = dotenv::var("HYPOTHESIS_KEY")?;
313    /// #     let group_id = dotenv::var("TEST_GROUP_ID").unwrap_or("__world__".into());
314    /// let api = Hypothesis::new(&username, &developer_key)?;
315    /// let mut annotation = api.create_annotation(&InputAnnotation::builder()
316    ///                   .text("string")
317    ///                   .uri("http://example.com")
318    ///                   .tags(vec!["tag1".to_string(), "tag2".to_string()])
319    ///                   .group(&group_id)
320    ///                   .build()?).await?;
321    /// annotation.text = String::from("New String");
322    /// let updated_annotation = api.update_annotation(&annotation).await?;
323    /// assert_eq!(updated_annotation.id, annotation.id);
324    /// assert_eq!(&updated_annotation.text, "New String");
325    /// #    api.delete_annotation(&updated_annotation.id).await?;
326    /// #    Ok(())
327    /// # }
328    /// ```
329    pub async fn update_annotation(
330        &self,
331        annotation: &Annotation,
332    ) -> Result<Annotation, HypothesisError> {
333        let text = self
334            .client
335            .patch(&format!("{}/annotations/{}", API_URL, annotation.id))
336            .json(&annotation)
337            .send()
338            .await
339            .map_err(HypothesisError::ReqwestError)?
340            .text()
341            .await
342            .map_err(HypothesisError::ReqwestError)?;
343        serde_parse::<Annotation>(&text)
344    }
345
346    /// Update many annotations at once
347    pub async fn update_annotations(
348        &self,
349        annotations: &[Annotation],
350    ) -> Result<Vec<Annotation>, HypothesisError> {
351        let futures: Vec<_> = annotations
352            .iter()
353            .map(|a| self.update_annotation(a))
354            .collect();
355        async { try_join_all(futures).await }.await
356    }
357
358    /// Search for annotations with optional filters
359    ///
360    /// Returns a list of annotations matching the search query.
361    /// See  [`SearchQuery`](annotations/struct.SearchQuery.html) for more filtering options
362    ///
363    /// This returns a max of 50 annotations at once, use `search_annotations_return_all` if you expect more
364    /// # Example
365    /// ```
366    /// # #[tokio::main]
367    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
368    /// use hypothesis::{Hypothesis, UserAccountID};
369    /// use hypothesis::annotations::SearchQuery;
370    /// #     dotenv::dotenv()?;
371    /// #     let username = dotenv::var("HYPOTHESIS_NAME")?;
372    /// #     let developer_key = dotenv::var("HYPOTHESIS_KEY")?;
373    /// let api = Hypothesis::new(&username, &developer_key)?;
374    /// /// Search for your own annotations:
375    /// let search_query = SearchQuery::builder().user(&api.user.0).build()?;
376    /// let search_results = api.search_annotations(&search_query).await?;
377    /// #     assert!(!search_results.is_empty());
378    /// #     Ok(())
379    /// # }
380    /// ```
381    pub async fn search_annotations(
382        &self,
383        query: &SearchQuery,
384    ) -> Result<Vec<Annotation>, HypothesisError> {
385        let query: HashMap<String, serde_json::Value> = serde_json::from_str(
386            &serde_json::to_string(&query).map_err(HypothesisError::SerdeError)?,
387        )
388        .map_err(HypothesisError::SerdeError)?;
389        let url = Url::parse_with_params(
390            &format!("{}/search", API_URL),
391            &query
392                .into_iter()
393                .map(|(k, v)| (k, v.to_string().replace('"', "")))
394                .collect::<Vec<_>>(),
395        )
396        .map_err(HypothesisError::URLError)?;
397        let text = self
398            .client
399            .get(url)
400            .send()
401            .await
402            .map_err(HypothesisError::ReqwestError)?
403            .text()
404            .await
405            .map_err(HypothesisError::ReqwestError)?;
406        #[derive(Deserialize, Debug, Clone, PartialEq)]
407        struct SearchResult {
408            rows: Vec<Annotation>,
409            total: usize,
410        }
411        Ok(serde_parse::<SearchResult>(&text)?.rows)
412    }
413
414    /// Retrieve all annotations matching query
415    /// See  [`SearchQuery`](annotations/struct.SearchQuery.html) for filtering options
416    pub async fn search_annotations_return_all(
417        &self,
418        query: &mut SearchQuery,
419    ) -> Result<Vec<Annotation>, HypothesisError> {
420        let mut annotations = Vec::new();
421        loop {
422            let next = self.search_annotations(query).await?;
423            if next.is_empty() {
424                break;
425            }
426            query.search_after = next[next.len() - 1]
427                .updated
428                .format(&Rfc3339)
429                .map_err(time::Error::Format)?;
430            annotations.extend_from_slice(&next);
431        }
432        Ok(annotations)
433    }
434
435    /// Fetch annotation by ID
436    ///
437    /// # Example
438    /// ```
439    /// # #[tokio::main]
440    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
441    /// use hypothesis::Hypothesis;
442    /// #    use hypothesis::annotations::InputAnnotation;
443    /// #    dotenv::dotenv()?;
444    /// #    let username = dotenv::var("HYPOTHESIS_NAME")?;
445    /// #    let developer_key = dotenv::var("HYPOTHESIS_KEY")?;
446    /// #    let group_id = dotenv::var("TEST_GROUP_ID").unwrap_or("__world__".into());
447    /// let api = Hypothesis::new(&username, &developer_key)?;
448    /// #    let annotation = api.create_annotation(&InputAnnotation::builder()
449    /// #                       .text("string")
450    /// #                       .uri("http://example.com")
451    /// #                       .group(group_id).build()?).await?;
452    /// #    let annotation_id = annotation.id.to_owned();    
453    /// let annotation = api.fetch_annotation(&annotation_id).await?;
454    /// assert_eq!(annotation.id, annotation_id);
455    /// #    api.delete_annotation(&annotation.id).await?;
456    /// #    Ok(())
457    /// # }
458    /// ```
459    pub async fn fetch_annotation(&self, id: &str) -> Result<Annotation, HypothesisError> {
460        let text = self
461            .client
462            .get(&format!("{}/annotations/{}", API_URL, id))
463            .send()
464            .await
465            .map_err(HypothesisError::ReqwestError)?
466            .text()
467            .await
468            .map_err(HypothesisError::ReqwestError)?;
469        serde_parse::<Annotation>(&text)
470    }
471
472    /// Fetch multiple annotations by ID
473    pub async fn fetch_annotations(
474        &self,
475        ids: &[String],
476    ) -> Result<Vec<Annotation>, HypothesisError> {
477        let futures: Vec<_> = ids.iter().map(|id| self.fetch_annotation(id)).collect();
478        try_join_all(futures).await
479    }
480
481    /// Delete annotation by ID
482    ///
483    /// # Example
484    /// ```
485    /// # #[tokio::main]
486    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
487    /// use hypothesis::Hypothesis;
488    /// #    use hypothesis::annotations::InputAnnotation;
489    /// #    dotenv::dotenv()?;
490    /// #    let username = dotenv::var("HYPOTHESIS_NAME")?;
491    /// #    let developer_key = dotenv::var("HYPOTHESIS_KEY")?;
492    /// #    let group_id = dotenv::var("TEST_GROUP_ID").unwrap_or("__world__".into());
493    /// let api = Hypothesis::new(&username, &developer_key)?;
494    /// #    let annotation = api.create_annotation(&InputAnnotation::builder()
495    /// #                       .text("string")
496    /// #                       .uri("http://example.com")
497    /// #                       .group(group_id).build()?).await?;
498    /// #    let annotation_id = annotation.id.to_owned();    
499    /// let deleted = api.delete_annotation(&annotation_id).await?;
500    /// assert!(deleted);
501    /// assert!(api.fetch_annotation(&annotation_id).await.is_err());
502    /// #    Ok(())
503    /// # }
504    /// ```
505    pub async fn delete_annotation(&self, id: &str) -> Result<bool, HypothesisError> {
506        let text = self
507            .client
508            .delete(&format!("{}/annotations/{}", API_URL, id))
509            .send()
510            .await
511            .map_err(HypothesisError::ReqwestError)?
512            .text()
513            .await
514            .map_err(HypothesisError::ReqwestError)?;
515        #[derive(Deserialize, Debug, Clone, PartialEq)]
516        struct DeletionResult {
517            id: String,
518            deleted: bool,
519        }
520        Ok(serde_parse::<DeletionResult>(&text)?.deleted)
521    }
522
523    /// Delete multiple annotations by ID
524    pub async fn delete_annotations(&self, ids: &[String]) -> Result<Vec<bool>, HypothesisError> {
525        let futures: Vec<_> = ids.iter().map(|id| self.delete_annotation(id)).collect();
526        try_join_all(futures).await
527    }
528
529    /// Flag an annotation
530    ///
531    /// Flag an annotation for review (moderation). The moderator of the group containing the
532    /// annotation will be notified of the flag and can decide whether or not to hide the
533    /// annotation. Note that flags persist and cannot be removed once they are set.
534    pub async fn flag_annotation(&self, id: &str) -> Result<(), HypothesisError> {
535        let text = self
536            .client
537            .put(&format!("{}/annotations/{}/flag", API_URL, id))
538            .send()
539            .await
540            .map_err(HypothesisError::ReqwestError)?
541            .text()
542            .await
543            .map_err(HypothesisError::ReqwestError)?;
544        let error = serde_json::from_str::<errors::APIError>(&text);
545        if let Ok(error) = error {
546            Err(HypothesisError::APIError {
547                source: error,
548                raw_text: text,
549                serde_error: None,
550            })
551        } else {
552            Ok(())
553        }
554    }
555
556    /// Hide an annotation
557    ///
558    /// Hide an annotation. The authenticated user needs to have the moderate permission for the
559    /// group that contains the annotation — this permission is granted to the user who created the group.
560    pub async fn hide_annotation(&self, id: &str) -> Result<(), HypothesisError> {
561        let text = self
562            .client
563            .put(&format!("{}/annotations/{}/hide", API_URL, id))
564            .send()
565            .await
566            .map_err(HypothesisError::ReqwestError)?
567            .text()
568            .await
569            .map_err(HypothesisError::ReqwestError)?;
570        let error = serde_json::from_str::<errors::APIError>(&text);
571        if let Ok(error) = error {
572            Err(HypothesisError::APIError {
573                source: error,
574                raw_text: text,
575                serde_error: None,
576            })
577        } else {
578            Ok(())
579        }
580    }
581
582    /// Show an annotation
583    ///
584    /// Show/"un-hide" an annotation. The authenticated user needs to have the moderate permission
585    /// for the group that contains the annotation—this permission is granted to the user who created the group.
586    pub async fn show_annotation(&self, id: &str) -> Result<(), HypothesisError> {
587        let text = self
588            .client
589            .delete(&format!("{}/annotations/{}/hide", API_URL, id))
590            .send()
591            .await
592            .map_err(HypothesisError::ReqwestError)?
593            .text()
594            .await
595            .map_err(HypothesisError::ReqwestError)?;
596        let error = serde_json::from_str::<errors::APIError>(&text);
597        if let Ok(error) = error {
598            Err(HypothesisError::APIError {
599                source: error,
600                raw_text: text,
601                serde_error: None,
602            })
603        } else {
604            Ok(())
605        }
606    }
607
608    /// Retrieve a list of applicable Groups, filtered by authority and target document (`document_uri`).
609    /// Also retrieve user's private Groups.
610    ///
611    /// # Example
612    /// ```
613    /// # #[tokio::main]
614    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
615    /// use hypothesis::Hypothesis;
616    /// use hypothesis::groups::GroupFilters;
617    /// #     dotenv::dotenv()?;
618    /// #     let username = dotenv::var("HYPOTHESIS_NAME")?;
619    /// #     let developer_key = dotenv::var("HYPOTHESIS_KEY")?;
620    ///
621    /// let api = Hypothesis::new(&username, &developer_key)?;
622    /// /// Get all Groups belonging to user
623    /// let groups = api.get_groups(&GroupFilters::default()).await?;
624    /// #    assert!(!groups.is_empty());
625    /// #    Ok(())
626    /// # }
627    /// ```
628    pub async fn get_groups(&self, query: &GroupFilters) -> Result<Vec<Group>, HypothesisError> {
629        let query: HashMap<String, serde_json::Value> = serde_json::from_str(
630            &serde_json::to_string(&query).map_err(HypothesisError::SerdeError)?,
631        )
632        .map_err(HypothesisError::SerdeError)?;
633        let url = Url::parse_with_params(
634            &format!("{}/groups", API_URL),
635            &query
636                .into_iter()
637                .map(|(k, v)| (k, v.to_string().replace('"', "")))
638                .collect::<Vec<_>>(),
639        )
640        .map_err(HypothesisError::URLError)?;
641        let text = self
642            .client
643            .get(url)
644            .send()
645            .await
646            .map_err(HypothesisError::ReqwestError)?
647            .text()
648            .await
649            .map_err(HypothesisError::ReqwestError)?;
650        serde_parse(&text)
651    }
652
653    /// Create a new, private group for the currently-authenticated user.
654    ///
655    /// # Example
656    /// ```no_run
657    /// # #[tokio::main]
658    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
659    /// use hypothesis::Hypothesis;
660    /// #     dotenv::dotenv()?;
661    /// #     let username = dotenv::var("HYPOTHESIS_NAME")?;
662    /// #     let developer_key = dotenv::var("HYPOTHESIS_KEY")?;
663    ///
664    /// let api = Hypothesis::new(&username, &developer_key)?;
665    /// let group = api.create_group("my_group", Some("a test group")).await?;
666    /// #    Ok(())
667    /// # }
668    /// ```
669    pub async fn create_group(
670        &self,
671        name: &str,
672        description: Option<&str>,
673    ) -> Result<Group, HypothesisError> {
674        let mut params = HashMap::new();
675        params.insert("name", name);
676        if let Some(description) = description {
677            params.insert("description", description);
678        }
679        let text = self
680            .client
681            .post(&format!("{}/groups", API_URL))
682            .json(&params)
683            .send()
684            .await
685            .map_err(HypothesisError::ReqwestError)?
686            .text()
687            .await
688            .map_err(HypothesisError::ReqwestError)?;
689        serde_parse(&text)
690    }
691
692    /// Create multiple groups
693    pub async fn create_groups(
694        &self,
695        names: &[String],
696        descriptions: &[Option<String>],
697    ) -> Result<Vec<Group>, HypothesisError> {
698        let futures: Vec<_> = names
699            .iter()
700            .zip(descriptions.iter())
701            .map(|(name, description)| self.create_group(name, description.as_deref()))
702            .collect();
703        async { try_join_all(futures).await }.await
704    }
705
706    /// Fetch a single Group resource.
707    ///
708    /// # Example
709    /// ```
710    /// # #[tokio::main]
711    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
712    /// use hypothesis::Hypothesis;
713    /// use hypothesis::groups::Expand;
714    /// #     dotenv::dotenv()?;
715    /// #     let username = dotenv::var("HYPOTHESIS_NAME")?;
716    /// #     let developer_key = dotenv::var("HYPOTHESIS_KEY")?;
717    /// #     let group_id = dotenv::var("TEST_GROUP_ID")?;
718    ///
719    /// let api = Hypothesis::new(&username, &developer_key)?;
720    /// /// Expands organization into a struct
721    /// let group = api.fetch_group(&group_id, vec![Expand::Organization]).await?;
722    /// #    Ok(())
723    /// # }    
724    /// ```
725    pub async fn fetch_group(
726        &self,
727        id: &str,
728        expand: Vec<Expand>,
729    ) -> Result<Group, HypothesisError> {
730        let params: HashMap<&str, Vec<String>> = if !expand.is_empty() {
731            vec![(
732                "expand",
733                expand
734                    .into_iter()
735                    .map(|e| serde_json::to_string(&e))
736                    .collect::<Result<_, _>>()
737                    .map_err(HypothesisError::SerdeError)?,
738            )]
739            .into_iter()
740            .collect()
741        } else {
742            HashMap::new()
743        };
744        let text = self
745            .client
746            .get(&format!("{}/groups/{}", API_URL, id))
747            .json(&params)
748            .send()
749            .await
750            .map_err(HypothesisError::ReqwestError)?
751            .text()
752            .await
753            .map_err(HypothesisError::ReqwestError)?;
754        serde_parse::<Group>(&text)
755    }
756
757    /// Fetch multiple groups by ID
758    pub async fn fetch_groups(
759        &self,
760        ids: &[String],
761        expands: Vec<Vec<Expand>>,
762    ) -> Result<Vec<Group>, HypothesisError> {
763        let futures: Vec<_> = ids
764            .iter()
765            .zip(expands.into_iter())
766            .map(|(id, expand)| self.fetch_group(id, expand))
767            .collect();
768        async { try_join_all(futures).await }.await
769    }
770
771    /// Update a Group resource.
772    ///
773    /// # Example
774    /// ```no_run
775    /// # #[tokio::main]
776    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
777    /// use hypothesis::Hypothesis;
778    /// #     dotenv::dotenv()?;
779    /// #     let username = dotenv::var("HYPOTHESIS_NAME")?;
780    /// #     let developer_key = dotenv::var("HYPOTHESIS_KEY")?;
781    /// #     let group_id = dotenv::var("TEST_GROUP_ID")?;
782    ///
783    /// let api = Hypothesis::new(&username, &developer_key)?;
784    /// let group = api.update_group(&group_id, Some("new_group_name"), None).await?;
785    /// assert_eq!(&group.name, "new_group_name");
786    /// assert_eq!(group.id, group_id);
787    /// #    Ok(())
788    /// # }
789    /// ```
790    pub async fn update_group(
791        &self,
792        id: &str,
793        name: Option<&str>,
794        description: Option<&str>,
795    ) -> Result<Group, HypothesisError> {
796        let mut params = HashMap::new();
797        if let Some(name) = name {
798            params.insert("name", name);
799        }
800        if let Some(description) = description {
801            params.insert("description", description);
802        }
803        let text = self
804            .client
805            .patch(&format!("{}/groups/{}", API_URL, id))
806            .json(&params)
807            .send()
808            .await
809            .map_err(HypothesisError::ReqwestError)?
810            .text()
811            .await
812            .map_err(HypothesisError::ReqwestError)?;
813        serde_parse::<Group>(&text)
814    }
815
816    /// Update multiple groups
817    pub async fn update_groups(
818        &self,
819        ids: &[String],
820        names: &[Option<String>],
821        descriptions: &[Option<String>],
822    ) -> Result<Vec<Group>, HypothesisError> {
823        let futures: Vec<_> = ids
824            .iter()
825            .zip(names.iter())
826            .zip(descriptions.iter())
827            .map(|((id, name), description)| {
828                self.update_group(id, name.as_deref(), description.as_deref())
829            })
830            .collect();
831        async { try_join_all(futures).await }.await
832    }
833
834    /// Fetch a list of all members (users) in a group. Returned user resource only contains public-facing user data.
835    /// Authenticated user must have read access to the group. Does not require authentication for reading members of
836    /// public groups. Returned members are unsorted.
837    ///
838    /// # Example
839    /// ```
840    /// # #[tokio::main]
841    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
842    /// use hypothesis::Hypothesis;
843    /// #     dotenv::dotenv()?;
844    /// #     let username = dotenv::var("HYPOTHESIS_NAME")?;
845    /// #     let developer_key = dotenv::var("HYPOTHESIS_KEY")?;
846    /// #     let group_id = dotenv::var("TEST_GROUP_ID")?;
847    ///
848    /// let api = Hypothesis::new(&username, &developer_key)?;
849    /// let members = api.get_group_members(&group_id).await?;
850    /// #    Ok(())
851    /// # }
852    /// ```
853    pub async fn get_group_members(&self, id: &str) -> Result<Vec<Member>, HypothesisError> {
854        let text = self
855            .client
856            .get(&format!("{}/groups/{}/members", API_URL, id))
857            .send()
858            .await
859            .map_err(HypothesisError::ReqwestError)?
860            .text()
861            .await
862            .map_err(HypothesisError::ReqwestError)?;
863        serde_parse::<Vec<Member>>(&text)
864    }
865
866    /// Remove yourself from a group.
867    pub async fn leave_group(&self, id: &str) -> Result<(), HypothesisError> {
868        let text = self
869            .client
870            .delete(&format!("{}/groups/{}/members/me", API_URL, id))
871            .send()
872            .await
873            .map_err(HypothesisError::ReqwestError)?
874            .text()
875            .await
876            .map_err(HypothesisError::ReqwestError)?;
877        let error = serde_json::from_str::<errors::APIError>(&text);
878        if let Ok(error) = error {
879            Err(HypothesisError::APIError {
880                source: error,
881                raw_text: text,
882                serde_error: None,
883            })
884        } else {
885            Ok(())
886        }
887    }
888
889    /// Fetch profile information for the currently-authenticated user.
890    ///
891    /// # Example
892    /// ```
893    /// # #[tokio::main]
894    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
895    /// use hypothesis::Hypothesis;
896    /// #     dotenv::dotenv()?;
897    /// #     let username = dotenv::var("HYPOTHESIS_NAME")?;
898    /// #     let developer_key = dotenv::var("HYPOTHESIS_KEY")?;
899    /// let api = Hypothesis::new(&username, &developer_key)?;
900    /// let profile = api.fetch_user_profile().await?;
901    /// assert!(profile.userid.is_some());
902    /// assert_eq!(profile.userid.unwrap(), api.user);
903    /// #     Ok(())
904    /// # }
905    /// ```
906
907    pub async fn fetch_user_profile(&self) -> Result<UserProfile, HypothesisError> {
908        let text = self
909            .client
910            .get(&format!("{}/profile", API_URL))
911            .send()
912            .await
913            .map_err(HypothesisError::ReqwestError)?
914            .text()
915            .await
916            .map_err(HypothesisError::ReqwestError)?;
917        serde_parse::<UserProfile>(&text)
918    }
919
920    /// Fetch the groups for which the currently-authenticated user is a member.
921    /// # Example
922    /// ```
923    /// # #[tokio::main]
924    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
925    /// use hypothesis::Hypothesis;
926    /// #     dotenv::dotenv()?;
927    /// #     let username = dotenv::var("HYPOTHESIS_NAME")?;
928    /// #     let developer_key = dotenv::var("HYPOTHESIS_KEY")?;
929    /// let api = Hypothesis::new(&username, &developer_key)?;
930    /// let groups = api.fetch_user_groups().await?;
931    /// #     Ok(())
932    /// # }
933    /// ```
934    pub async fn fetch_user_groups(&self) -> Result<Vec<Group>, HypothesisError> {
935        let text = self
936            .client
937            .get(&format!("{}/profile/groups", API_URL))
938            .send()
939            .await
940            .map_err(HypothesisError::ReqwestError)?
941            .text()
942            .await
943            .map_err(HypothesisError::ReqwestError)?;
944        serde_parse::<Vec<Group>>(&text)
945    }
946}
947
948/// Stores user account ID in the form "acct:{username}@hypothes.is"
949///
950/// Create from username:
951/// ```
952/// # use hypothesis::UserAccountID;
953/// let user_id = "my_username".parse::<UserAccountID>().unwrap();
954/// ```
955#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
956pub struct UserAccountID(pub String);
957
958impl FromStr for UserAccountID {
959    type Err = ParseError;
960    fn from_str(s: &str) -> Result<Self, Self::Err> {
961        Ok(Self(format!("acct:{}@hypothes.is", s)))
962    }
963}
964
965impl fmt::Display for UserAccountID {
966    #[inline]
967    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
968        write!(f, "{}", self.0)
969    }
970}
971
972impl From<&UserAccountID> for UserAccountID {
973    #[inline]
974    fn from(a: &UserAccountID) -> UserAccountID {
975        UserAccountID(a.0.to_owned())
976    }
977}