hypothesis_rs/
annotations.rs

1//! Objects related to the "annotations" endpoint
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6#[cfg(feature = "cli")]
7use structopt::StructOpt;
8use time::OffsetDateTime;
9
10use crate::{errors, is_default, UserAccountID};
11
12#[cfg_attr(feature = "cli", derive(StructOpt))]
13#[cfg_attr(
14    feature = "cli",
15    structopt(
16        about = "Create an annotation",
17        long_about = "Create and upload an annotation to your Hypothesis"
18    )
19)]
20/// Struct to create annotations
21///
22/// All fields except uri are optional, i.e. leave as default.
23///
24/// # Example
25/// ```
26/// use hypothesis::annotations::{InputAnnotation, Target, Selector};
27/// # #[tokio::main]
28/// # async fn main() -> Result<(), hypothesis::errors::HypothesisError> {
29/// // A simple annotation
30/// let annotation_simple = InputAnnotation::builder()
31///     .uri("https://www.example.com")
32///     .text("My new annotation").build()?;
33///
34/// // A complex annotation
35/// let annotation_complex = InputAnnotation::builder()
36///     .uri("https://www.example.com")
37///     .text("this is a comment")
38///     .target(Target::builder().source("https://www.example.com")
39///         .selector(vec![Selector::new_quote("exact text in website to highlight",
40///                                             "prefix of text",
41///                                             "suffix of text")]).build()?)
42///     .tags(vec!["tag1".into(), "tag2".into()])
43///     .build()?;
44/// # Ok(())
45/// # }
46/// ```
47#[derive(Serialize, Debug, Default, Clone, Builder, PartialEq)]
48#[builder(default, build_fn(name = "builder"))]
49pub struct InputAnnotation {
50    /// URI that this annotation is attached to.
51    ///
52    /// Can be a URL (a web page address) or a URN representing another kind of resource such as
53    /// DOI (Digital Object Identifier) or a PDF fingerprint.
54    #[serde(skip_serializing_if = "is_default")]
55    #[cfg_attr(feature = "cli", structopt(default_value))]
56    #[builder(setter(into))]
57    pub uri: String,
58    /// Annotation text / comment given by user
59    ///
60    /// This is NOT the selected text on the web-page
61    #[serde(skip_serializing_if = "is_default")]
62    #[cfg_attr(feature = "cli", structopt(default_value, long))]
63    #[builder(setter(into))]
64    pub text: String,
65    /// Tags attached to the annotation
66    #[serde(skip_serializing_if = "is_default")]
67    #[cfg_attr(feature = "cli", structopt(long))]
68    #[builder(setter(strip_option), default)]
69    pub tags: Option<Vec<String>>,
70    /// Further metadata about the target document
71    #[serde(skip_serializing_if = "is_default")]
72    #[cfg_attr(feature = "cli", structopt(skip))]
73    #[builder(setter(strip_option), default)]
74    pub document: Option<Document>,
75    #[serde(skip_serializing_if = "is_default")]
76    /// The unique identifier for the annotation's group.
77    ///
78    /// If an annotation is a reply to another
79    /// annotation (see `references`), this field will be ignored —
80    /// replies belong to the same group as their parent annotations.
81    #[cfg_attr(feature = "cli", structopt(default_value, long))]
82    #[builder(setter(into))]
83    pub group: String,
84    /// Which part of the document does the annotation target?
85    ///
86    /// If left as default then the annotation is linked to the whole page.
87    #[serde(skip_serializing_if = "is_default")]
88    #[cfg_attr(feature = "cli", structopt(skip))]
89    pub target: Target,
90    /// Annotation IDs for any annotations this annotation references (e.g. is a reply to)
91    #[serde(skip_serializing_if = "is_default")]
92    #[cfg_attr(feature = "cli", structopt(long))]
93    pub references: Vec<String>,
94}
95
96impl InputAnnotation {
97    pub fn builder() -> InputAnnotationBuilder {
98        InputAnnotationBuilder::default()
99    }
100}
101
102impl InputAnnotationBuilder {
103    /// Builds a new `InputAnnotation`.
104    pub fn build(&self) -> Result<InputAnnotation, errors::HypothesisError> {
105        self.builder()
106            .map_err(|e| errors::HypothesisError::BuilderError(e.to_string()))
107    }
108}
109
110impl Annotation {
111    pub fn update(&mut self, annotation: InputAnnotation) {
112        if !annotation.uri.is_empty() {
113            self.uri = annotation.uri;
114        }
115        if !annotation.text.is_empty() {
116            self.text = annotation.text;
117        }
118        if let Some(tags) = annotation.tags {
119            self.tags = tags;
120        }
121        if !annotation.group.is_empty() {
122            self.group = annotation.group;
123        }
124        if annotation.references.is_empty() {
125            self.references = annotation.references;
126        }
127    }
128}
129
130#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Builder)]
131#[builder(build_fn(name = "builder"))]
132pub struct Document {
133    #[serde(skip_serializing_if = "is_default", default)]
134    pub title: Vec<String>,
135    #[serde(skip_serializing_if = "is_default", default)]
136    #[builder(setter(strip_option), default)]
137    pub dc: Option<Dc>,
138    #[serde(skip_serializing_if = "is_default", default)]
139    #[builder(setter(strip_option), default)]
140    pub highwire: Option<HighWire>,
141    #[serde(skip_serializing_if = "is_default", default)]
142    pub link: Vec<Link>,
143}
144
145impl Document {
146    pub fn builder() -> DocumentBuilder {
147        DocumentBuilder::default()
148    }
149}
150
151impl DocumentBuilder {
152    /// Builds a new `Document`.
153    pub fn build(&self) -> Result<Document, errors::HypothesisError> {
154        self.builder()
155            .map_err(|e| errors::HypothesisError::BuilderError(e.to_string()))
156    }
157}
158
159#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
160pub struct HighWire {
161    #[serde(skip_serializing_if = "is_default", default)]
162    pub doi: Vec<String>,
163    #[serde(skip_serializing_if = "is_default", default)]
164    pub pdf_url: Vec<String>,
165}
166
167#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
168pub struct Link {
169    pub href: String,
170    #[serde(skip_serializing_if = "is_default", rename = "type", default)]
171    pub link_type: String,
172}
173
174#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
175pub struct Dc {
176    #[serde(skip_serializing_if = "is_default", default)]
177    pub identifier: Vec<String>,
178}
179
180/// Full representation of an Annotation resource and applicable relationships.
181#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
182pub struct Annotation {
183    /// Annotation ID
184    pub id: String,
185    /// Date of creation
186    #[serde(with = "time::serde::rfc3339")]
187    pub created: OffsetDateTime,
188    /// Date of last update
189    #[serde(with = "time::serde::rfc3339")]
190    pub updated: OffsetDateTime,
191    /// User account ID in the format "acct:<username>@<authority>"
192    pub user: UserAccountID,
193    /// URL of document this annotation is attached to
194    pub uri: String,
195    /// The text content of the annotation body (NOT the selected text in the document)
196    pub text: String,
197    /// Tags attached to annotation
198    pub tags: Vec<String>,
199    /// The unique identifier for the annotation's group
200    pub group: String,
201    pub permissions: Permissions,
202    /// Which part of the document does the annotation target.
203    pub target: Vec<Target>,
204    /// An object containing hypermedia links for this annotation
205    pub links: HashMap<String, String>,
206    /// Whether this annotation is hidden from public view
207    pub hidden: bool,
208    /// Whether this annotation has one or more flags for moderation
209    pub flagged: bool,
210    /// Document information
211    #[serde(default)]
212    pub document: Option<Document>,
213    /// Annotation IDs for any annotations this annotation references (e.g. is a reply to)
214    #[serde(default)]
215    pub references: Vec<String>,
216    #[serde(default)]
217    pub user_info: Option<UserInfo>,
218}
219
220#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
221pub struct UserInfo {
222    /// The annotation creator's display name
223    pub display_name: Option<String>,
224}
225
226/// > While the API accepts arbitrary Annotation selectors in the target.selector property,
227/// > the Hypothesis client currently supports TextQuoteSelector, RangeSelector and TextPositionSelector selector.
228/// [Hypothesis API v1.0.0](https://h.readthedocs.io/en/latest/api-reference/v1/#tag/annotations/paths/~1annotations/post)
229#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Builder)]
230#[builder(build_fn(name = "builder"))]
231pub struct Target {
232    /// The target URI for the annotation
233    /// Leave empty when creating an annotation
234    #[serde(skip_serializing_if = "is_default")]
235    #[builder(setter(into))]
236    pub source: String,
237    /// An array of selectors that refine this annotation's target
238    #[serde(default, skip_serializing_if = "is_default")]
239    pub selector: Vec<Selector>,
240}
241
242impl Target {
243    pub fn builder() -> TargetBuilder {
244        TargetBuilder::default()
245    }
246}
247
248impl TargetBuilder {
249    /// Builds a new `Target`.
250    pub fn build(&self) -> Result<Target, errors::HypothesisError> {
251        self.builder()
252            .map_err(|e| errors::HypothesisError::BuilderError(e.to_string()))
253    }
254}
255
256/// > Many Annotations refer to part of a resource, rather than all of it, as the Target.
257/// > We call that part of the resource a Segment (of Interest). A Selector is used to describe how
258/// > to determine the Segment from within the Source resource.
259/// [Web Annotation Data Model - Selectors](https://www.w3.org/TR/annotation-model/#selectors)
260#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
261#[serde(tag = "type")]
262pub enum Selector {
263    TextQuoteSelector(TextQuoteSelector),
264    /// > Selections made by users may be extensive and/or cross over internal boundaries in the
265    /// > representation, making it difficult to construct a single selector that robustly describes
266    /// > the correct content. A Range Selector can be used to identify the beginning and the end of
267    /// > the selection by using other Selectors. In this way, two points can be accurately identified
268    /// > using the most appropriate selection mechanisms, and then linked together to form the selection.
269    /// > The selection consists of everything from the beginning of the starting selector through to the
270    /// > beginning of the ending selector, but not including it.
271    /// [Web Annotation Data Model - Range Selector](https://www.w3.org/TR/annotation-model/#range-selector)
272    /// NOTE - the Hypothesis API doesn't seem to follow this standard for RangeSelector so this just returns a HashMap for now
273    TextPositionSelector(TextPositionSelector),
274    /// TODO: make Selectors into structs
275    RangeSelector(HashMap<String, serde_json::Value>),
276    FragmentSelector(HashMap<String, serde_json::Value>),
277    CssSelector(HashMap<String, serde_json::Value>),
278    XPathSelector(HashMap<String, serde_json::Value>),
279    DataPositionSelector(HashMap<String, serde_json::Value>),
280    SvgSelector(HashMap<String, serde_json::Value>),
281}
282
283impl Selector {
284    pub fn new_quote(exact: &str, prefix: &str, suffix: &str) -> Self {
285        Self::TextQuoteSelector(TextQuoteSelector {
286            exact: exact.to_string(),
287            prefix: prefix.to_string(),
288            suffix: suffix.to_string(),
289        })
290    }
291}
292
293/// > This Selector describes a range of text by copying it, and including some of the text
294/// > immediately before (a prefix) and after (a suffix) it to distinguish between multiple
295/// > copies of the same sequence of characters.
296///
297/// > For example, if the document were again "abcdefghijklmnopqrstuvwxyz", one could select
298/// > "efg" by a prefix of "abcd", the match of "efg" and a suffix of "hijk".
299/// [Web Annotation Data Model - Text Quote Selector](https://www.w3.org/TR/annotation-model/#text-quote-selector)
300#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
301pub struct TextQuoteSelector {
302    /// A copy of the text which is being selected, after normalization.
303    pub exact: String,
304    /// A snippet of text that occurs immediately before the text which is being selected.
305    pub prefix: String,
306    /// The snippet of text that occurs immediately after the text which is being selected.
307    pub suffix: String,
308}
309
310/// >  This Selector describes a range of text by recording the start and end positions of the
311/// > selection in the stream. Position 0 would be immediately before the first character, position
312/// > 1 would be immediately before the second character, and so on. The start character is thus
313/// > included in the list, but the end character is not.
314///
315/// > For example, if the document was "abcdefghijklmnopqrstuvwxyz", the start was 4, and the end
316/// > was 7, then the selection would be "efg".
317/// [Web Annotation Data Model - Text Position Selector](https://www.w3.org/TR/annotation-model/#text-position-selector)
318#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
319pub struct TextPositionSelector {
320    /// The starting position of the segment of text. The first character in the full text is
321    /// character position 0, and the character is included within the segment.
322    pub start: u64,
323    /// The end position of the segment of text. The character is not included within the segment.
324    pub end: u64,
325}
326
327#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
328#[serde(rename_all = "lowercase")]
329pub enum Sort {
330    Created,
331    Updated,
332    Id,
333    Group,
334    User,
335}
336
337impl Default for Sort {
338    fn default() -> Self {
339        Self::Updated
340    }
341}
342
343#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
344#[serde(rename_all = "lowercase")]
345pub enum Order {
346    Asc,
347    Desc,
348}
349
350impl Default for Order {
351    fn default() -> Self {
352        Self::Desc
353    }
354}
355
356/// Options to filter and sort search results. See [the Hypothesis API docs](https://h.readthedocs.io/en/latest/api-reference/v1/#tag/annotations/paths/~1search/get) for more details on using these fields
357#[cfg_attr(feature = "cli", derive(StructOpt))]
358#[derive(Serialize, Debug, Clone, PartialEq, Builder, Default)]
359#[builder(build_fn(name = "builder"), default)]
360pub struct SearchQuery {
361    /// The maximum number of annotations to return.
362    ///
363    /// Default: 20. Range: [ 0 .. 200 ]
364    #[builder(default = "20")]
365    #[cfg_attr(feature = "cli", structopt(default_value = "20", long))]
366    pub limit: u8,
367    /// The field by which annotations should be sorted
368    /// One of created, updated, id, group, user
369    ///
370    /// Default: updated
371    #[cfg_attr(feature = "cli", structopt(default_value = "updated", long, possible_values = & Sort::variants()))]
372    pub sort: Sort,
373    /// Example: "2019-01-03T19:46:09.334Z"
374    ///
375    /// Define a start point for a subset (page) of annotation search results.
376    /// NOTE: make sure to set sort to `Sort::Asc` if using `search_after`
377    #[serde(skip_serializing_if = "is_default")]
378    #[cfg_attr(feature = "cli", structopt(default_value, long))]
379    #[builder(setter(into))]
380    pub search_after: String,
381    /// The number of initial annotations to skip in the result set.
382    ///
383    /// Default: 0. Range: <= 9800.
384    /// search_after is more efficient.
385    #[cfg_attr(feature = "cli", structopt(default_value = "0", long))]
386    pub offset: usize,
387    /// The order in which the results should be sorted.
388    /// One of asc, desc
389    ///
390    /// Default: desc
391    #[cfg_attr(feature = "cli", structopt(default_value = "desc", long, possible_values = & Order::variants()))]
392    pub order: Order,
393    /// Limit the results to annotations matching the specific URI or equivalent URIs.
394    ///
395    /// URI can be a URL (a web page address) or a URN representing another kind of resource such
396    /// as DOI (Digital Object Identifier) or a PDF fingerprint.
397    #[serde(skip_serializing_if = "is_default")]
398    #[cfg_attr(feature = "cli", structopt(default_value, long))]
399    #[builder(setter(into))]
400    pub uri: String,
401    /// Limit the results to annotations containing the given keyword (tokenized chunk) in the URI.
402    /// The value must exactly match an individual URI keyword.
403    ///
404    #[serde(rename = "uri.parts", skip_serializing_if = "is_default")]
405    #[cfg_attr(feature = "cli", structopt(default_value, long))]
406    #[builder(setter(into))]
407    pub uri_parts: String,
408    /// Limit the results to annotations whose URIs match the wildcard pattern.
409    #[serde(rename = "wildcard_uri", skip_serializing_if = "is_default")]
410    #[cfg_attr(feature = "cli", structopt(default_value, long))]
411    #[builder(setter(into))]
412    pub wildcard_uri: String,
413    /// Limit the results to annotations made by the specified user. (in the format `acct:<username>@<authority>`)
414    #[serde(skip_serializing_if = "is_default")]
415    #[cfg_attr(feature = "cli", structopt(default_value, long))]
416    #[builder(setter(into))]
417    pub user: String,
418    /// Limit the results to annotations made in the specified group (by group ID).
419    #[serde(skip_serializing_if = "is_default")]
420    #[cfg_attr(feature = "cli", structopt(default_value, long))]
421    #[builder(setter(into))]
422    pub group: String,
423    /// Limit the results to annotations tagged with the specified value.
424    #[serde(skip_serializing_if = "is_default")]
425    #[cfg_attr(feature = "cli", structopt(default_value, long))]
426    #[builder(setter(into))]
427    pub tag: String,
428    /// Similar to tag but allows a list of multiple tags.
429    #[serde(skip_serializing_if = "is_default")]
430    #[cfg_attr(feature = "cli", structopt(long))]
431    pub tags: Vec<String>,
432    /// Limit the results to annotations who contain the indicated keyword in any of the following fields:
433    /// `quote`, `tags`, `text`, `url`
434    #[serde(skip_serializing_if = "is_default")]
435    #[cfg_attr(feature = "cli", structopt(default_value, long))]
436    #[builder(setter(into))]
437    pub any: String,
438    /// Limit the results to annotations that contain this text inside the text that was annotated.
439    #[serde(skip_serializing_if = "is_default")]
440    #[cfg_attr(feature = "cli", structopt(default_value, long))]
441    #[builder(setter(into))]
442    pub quote: String,
443    /// Returns annotations that are replies to this parent annotation ID.
444    #[serde(skip_serializing_if = "is_default")]
445    #[cfg_attr(feature = "cli", structopt(default_value, long))]
446    #[builder(setter(into))]
447    pub references: String,
448    /// Limit the results to annotations that contain this text in their textual body.
449    #[serde(skip_serializing_if = "is_default")]
450    #[cfg_attr(feature = "cli", structopt(default_value, long))]
451    #[builder(setter(into))]
452    pub text: String,
453}
454
455impl SearchQuery {
456    pub fn builder() -> SearchQueryBuilder {
457        SearchQueryBuilder::default()
458    }
459}
460
461impl SearchQueryBuilder {
462    /// Builds a new `SearchQuery`.
463    pub fn build(&self) -> Result<SearchQuery, errors::HypothesisError> {
464        self.builder()
465            .map_err(|e| errors::HypothesisError::BuilderError(e.to_string()))
466    }
467}
468
469#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
470pub struct Permissions {
471    pub read: Vec<String>,
472    pub delete: Vec<String>,
473    pub admin: Vec<String>,
474    pub update: Vec<String>,
475}