hypothesis_rs/lib.rs
1//! [](https://crates.io/crates/hypothesis)
2//! [](https://docs.rs/hypothesis)
3//! [](https://github.com/out-of-cheese-error/rust-hypothesis/actions)
4//! [](https://GitHub.com/out-of-cheese-error/rust-hypothesis/releases/)
5//! [](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(¶ms)
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(¶ms)
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(¶ms)
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}