1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
use crate::errors::APIError;
use crate::{is_default, GroupID, Hypothesis, API_URL};
use color_eyre::Help;
use eyre::WrapErr;
use reqwest::Url;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[cfg(feature = "cli")]
use structopt::StructOpt;

impl Hypothesis {
    /// Retrieve a list of applicable Groups, filtered by authority and target document (document_uri).
    /// Also retrieve user's private Groups.
    ///
    /// # Example
    /// ```
    /// # fn main() -> color_eyre::Result<()> {
    /// use hypothesis::Hypothesis;
    /// use hypothesis::groups::GroupFilters;
    /// #     dotenv::dotenv()?;
    /// #     let username = dotenv::var("USERNAME")?;
    /// #     let developer_key = dotenv::var("DEVELOPER_KEY")?;
    ///
    /// let api = Hypothesis::new(&username, &developer_key)?;
    /// /// Get all Groups belonging to user
    /// let groups = api.get_groups(&GroupFilters::default())?;
    /// #    assert!(!groups.is_empty());
    /// #    Ok(())
    /// # }
    /// ```
    pub fn get_groups(&self, query: &GroupFilters) -> color_eyre::Result<Vec<Group>> {
        let query: HashMap<String, serde_json::Value> =
            serde_json::from_str(&serde_json::to_string(&query)?)?;
        let url = Url::parse_with_params(
            &format!("{}/groups", API_URL),
            &query
                .into_iter()
                .map(|(k, v)| (k, v.to_string().replace('"', "")))
                .collect::<Vec<_>>(),
        )?;
        let text = self.client.get(url).send()?.text()?;
        let result = serde_json::from_str::<Vec<Group>>(&text)
            .wrap_err(serde_json::from_str::<APIError>(&text).unwrap_or_default())
            .suggestion("Make sure input filters are valid");
        Ok(result?)
    }

    /// Create a new, private group for the currently-authenticated user.
    ///
    /// # Example
    /// ```no_run
    /// # fn main() -> color_eyre::Result<()> {
    /// use hypothesis::Hypothesis;
    /// #     dotenv::dotenv()?;
    /// #     let username = dotenv::var("USERNAME")?;
    /// #     let developer_key = dotenv::var("DEVELOPER_KEY")?;
    ///
    /// let api = Hypothesis::new(&username, &developer_key)?;
    /// let group = api.create_group("my_group", Some("a test group"))?;
    /// #    Ok(())
    /// # }
    /// ```
    pub fn create_group(&self, name: &str, description: Option<&str>) -> color_eyre::Result<Group> {
        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(&params)
            .send()?
            .text()?;
        let result = serde_json::from_str::<Group>(&text)
            .wrap_err(serde_json::from_str::<APIError>(&text).unwrap_or_default())
            .suggestion("OutOfCheeseError: Redo from start.");
        Ok(result?)
    }

    /// Fetch a single Group resource.
    ///
    /// # Example
    /// ```
    /// # fn main() -> color_eyre::Result<()> {
    /// use hypothesis::Hypothesis;
    /// use hypothesis::groups::Expand;
    /// #     dotenv::dotenv()?;
    /// #     let username = dotenv::var("USERNAME")?;
    /// #     let developer_key = dotenv::var("DEVELOPER_KEY")?;
    /// #     let group_id = dotenv::var("TEST_GROUP_ID")?;
    ///
    /// let api = Hypothesis::new(&username, &developer_key)?;
    /// /// Expands organization into a struct
    /// let group = api.fetch_group(&group_id, vec![Expand::Organization])?;
    /// #    Ok(())
    /// # }    
    /// ```
    pub fn fetch_group(&self, id: &GroupID, expand: Vec<Expand>) -> color_eyre::Result<Group> {
        let params: HashMap<&str, Vec<String>> = if !expand.is_empty() {
            vec![(
                "expand",
                expand
                    .into_iter()
                    .map(|e| serde_json::to_string(&e))
                    .collect::<Result<_, _>>()?,
            )]
            .into_iter()
            .collect()
        } else {
            HashMap::new()
        };
        let text = self
            .client
            .get(&format!("{}/groups/{}", API_URL, id))
            .json(&params)
            .send()?
            .text()?;
        let result = serde_json::from_str::<Group>(&text)
            .wrap_err(serde_json::from_str::<APIError>(&text).unwrap_or_default())
            .suggestion("Make sure the given GroupId exists");
        Ok(result?)
    }

    /// Update a Group resource.
    ///
    /// # Example
    /// ```no_run
    /// # fn main() -> color_eyre::Result<()> {
    /// use hypothesis::Hypothesis;
    /// #     dotenv::dotenv()?;
    /// #     let username = dotenv::var("USERNAME")?;
    /// #     let developer_key = dotenv::var("DEVELOPER_KEY")?;
    /// #     let group_id = dotenv::var("TEST_GROUP_ID")?;
    ///
    /// let api = Hypothesis::new(&username, &developer_key)?;
    /// let group = api.update_group(&group_id, Some("new_group_name"), None)?;
    /// assert_eq!(&group.name, "new_group_name");
    /// assert_eq!(group.id, group_id);
    /// #    Ok(())
    /// # }
    /// ```
    pub fn update_group(
        &self,
        id: &GroupID,
        name: Option<&str>,
        description: Option<&str>,
    ) -> color_eyre::Result<Group> {
        let mut params = vec![];
        if let Some(name) = name {
            params.push(("name", name));
        }
        if let Some(description) = description {
            params.push(("description", description));
        }
        let text = self
            .client
            .patch(&format!("{}/groups/{}", API_URL, id))
            .form(&params)
            .send()?
            .text()?;
        let result = serde_json::from_str::<Group>(&text)
            .wrap_err(serde_json::from_str::<APIError>(&text).unwrap_or_default())
            .suggestion("Make sure the given GroupID exists");
        Ok(result?)
    }

    /// Fetch a list of all members (users) in a group. Returned user resource only contains public-facing user data.
    /// Authenticated user must have read access to the group. Does not require authentication for reading members of
    /// public groups. Returned members are unsorted.
    ///
    /// # Example
    /// ```
    /// # fn main() -> color_eyre::Result<()> {
    /// use hypothesis::Hypothesis;
    /// #     dotenv::dotenv()?;
    /// #     let username = dotenv::var("USERNAME")?;
    /// #     let developer_key = dotenv::var("DEVELOPER_KEY")?;
    /// #     let group_id = dotenv::var("TEST_GROUP_ID")?;
    ///
    /// let api = Hypothesis::new(&username, &developer_key)?;
    /// let members = api.get_group_members(&group_id)?;
    /// #    Ok(())
    /// # }
    /// ```
    pub fn get_group_members(&self, id: &GroupID) -> color_eyre::Result<Vec<GroupMember>> {
        let text = self
            .client
            .get(&format!("{}/groups/{}/members", API_URL, id))
            .send()?
            .text()?;
        let result = serde_json::from_str::<Vec<GroupMember>>(&text)
            .wrap_err(serde_json::from_str::<APIError>(&text).unwrap_or_default())
            .suggestion("Make sure the given GroupID exists");
        Ok(result?)
    }

    /// Remove yourself from a group.
    pub fn leave_group(&self, id: &GroupID) -> color_eyre::Result<()> {
        let text = self
            .client
            .delete(&format!("{}/groups/{}/members/me", API_URL, id))
            .send()?
            .text()?;
        let error = serde_json::from_str::<APIError>(&text);
        if let Ok(error) = error {
            Err(error).suggestion("Make sure the given GroupID exists")
        } else {
            Ok(())
        }
    }
}

/// Which field to expand
#[derive(Serialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Expand {
    Organization,
    Scopes,
}

/// Filter groups by authority and target document
#[cfg_attr(feature = "cli", derive(StructOpt))]
#[derive(Serialize, Debug, Default, Clone, PartialEq)]
pub struct GroupFilters {
    /// Filter returned groups to this authority.
    /// For authenticated requests, the user's associated authority will supersede any provided value.
    ///
    /// Default: "hypothes.is"
    #[serde(skip_serializing_if = "is_default")]
    #[cfg_attr(feature = "cli", structopt(default_value = "hypothes.is", long))]
    pub authority: String,
    /// Only retrieve public (i.e. non-private) groups that apply to a given document URI (i.e. the target document being annotated).
    #[serde(skip_serializing_if = "is_default")]
    #[cfg_attr(feature = "cli", structopt(default_value, long))]
    pub document_uri: String,
    /// One or more relations to expand for a group resource.
    /// Possible values: organization, scopes
    #[serde(skip_serializing_if = "is_default")]
    #[cfg_attr(feature = "cli", structopt(long, possible_values = & Expand::variants()))]
    pub expand: Vec<Expand>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Links {
    /// URL to the group's main (activity) page
    #[serde(default)]
    pub html: Option<String>,
}

/// See [the Hypothesis API docs](https://h.readthedocs.io/en/latest/api-reference/v1/#tag/groups/paths/~1groups/get) for more information.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Scope {
    pub enforced: bool,
    pub uri_patterns: Vec<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Type {
    Private,
    Open,
    Restricted,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)]
pub enum Organization {
    /// Unexpanded = Unique organization ID
    String(String),
    /// Expanded (None if not authorized)
    Organization(Option<Org>),
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Org {
    pub id: String,
    /// true if this organization is the default organization for the current authority
    pub default: bool,
    /// URI to logo image; may be null if no logo exists
    pub logo: Option<String>,
    pub name: String,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Group {
    /// Group ID
    pub id: GroupID,
    /// Authority-unique identifier that may be set for groups that are owned by a third-party authority.
    /// This field is currently present but unused for first-party-authority groups.
    pub groupid: Option<GroupID>,
    /// Group name
    pub name: String,
    pub links: Links,
    /// The organization to which this group belongs.
    pub organization: Organization,
    #[serde(default)]
    /// Information about the URL restrictions for annotations within this group.
    pub scopes: Option<Scope>,
    /// Whether or not this group has URL restrictions for documents that may be annotated within it.
    /// Non-scoped groups allow annotation to documents at any URL
    pub scoped: bool,
    #[serde(rename = "type")]
    pub group_type: Type,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct GroupMember {
    pub authority: String,
    /// // string [ 3 .. 30 ] characters ^[A-Za-z0-9._]+$
    pub username: String,
    /// string^acct:.+$
    pub userid: String,
    /// string <= 30 characters
    pub display_name: String,
}