ietfdata_rs/
lib.rs

1// Copyright (C) 2019 University of Glasgow
2// 
3// Redistribution and use in source and binary forms, with or without
4// modification, are permitted provided that the following conditions 
5// are met:
6// 
7// 1. Redistributions of source code must retain the above copyright notice,
8//    this list of conditions and the following disclaimer.
9// 
10// 2. Redistributions in binary form must reproduce the above copyright
11//    notice, this list of conditions and the following disclaimer in the
12//    documentation and/or other materials provided with the distribution.
13// 
14// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
18// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24// POSSIBILITY OF SUCH DAMAGE.
25//
26// SPDX-License-Identifier: BSD-2-Clause
27
28// This library contains code to interact with the IETF Datatracker
29// (https://datatracker.ietf.org/release/about)
30//
31// The Datatracker API is at https://datatracker.ietf.org/api/v1 and is
32// a REST API implemented using Django Tastypie (http://tastypieapi.org)
33//
34// It's possible to do time range queries on many of these values, for example:
35//   https://datatracker.ietf.org/api/v1/person/person/?time__gt=2018-03-27T14:07:36
36//
37// See also:
38//   https://datatracker.ietf.org/api/
39//   https://trac.tools.ietf.org/tools/ietfdb/wiki/DatabaseSchemaDescription
40//   https://trac.tools.ietf.org/tools/ietfdb/wiki/DatatrackerDrafts
41//   RFC 6174 "Definition of IETF Working Group Document States"
42//   RFC 6175 "Requirements to Extend the Datatracker for IETF Working Group Chairs and Authors"
43//   RFC 6292 "Requirements for a Working Group Charter Tool"
44//   RFC 6293 "Requirements for Internet-Draft Tracking by the IETF Community in the Datatracker"
45//   RFC 6322 "Datatracker States and Annotations for the IAB, IRTF, and Independent Submission Streams"
46//   RFC 6359 "Datatracker Extensions to Include IANA and RFC Editor Processing Information"
47//   RFC 7760 "Statement of Work for Extensions to the IETF Datatracker for Author Statistics"
48
49extern crate chrono;
50extern crate reqwest;
51extern crate serde;
52extern crate serde_json;
53
54use chrono::prelude::*;
55use serde::{Deserialize, Deserializer};
56use std::error;
57use std::fmt;
58
59// =================================================================================================================================
60// Helper functions:
61
62fn deserialize_time<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
63    where D: Deserializer<'de>
64{
65    let s = String::deserialize(deserializer)?;
66    Utc.datetime_from_str(&s, "%Y-%m-%dT%H:%M:%S%.f").map_err(serde::de::Error::custom)
67}
68
69// Generic types representing a paginated list of responses from the Datatracker:
70
71#[derive(Deserialize, Debug)]
72struct Meta {
73    total_count : u32,
74    limit       : u32,
75    offset      : u32,
76    previous    : Option<String>,
77    next        : Option<String>
78}
79
80#[derive(Deserialize, Debug)]
81struct Page<T> {
82    meta        : Meta,
83    objects     : Vec<T>
84}
85
86pub struct PaginatedList<'a, T> {
87    iter : <Vec<T> as IntoIterator>::IntoIter,
88    next : Option<String>,
89    dt   : &'a Datatracker
90}
91
92impl<'a, T> PaginatedList<'a, T>
93    where for<'de> T: Deserialize<'de>
94{
95    pub fn new(dt: &'a Datatracker, url : String) -> Result<Self, DatatrackerError> {
96        let mut res = dt.connection.get(&url).send()?;
97        let pl : Page<T> = res.json()?;
98
99        Ok(Self {
100            next : pl.meta.next.clone(),
101            iter : pl.objects.into_iter(),
102            dt   : dt
103        })
104    }
105
106    fn try_next(&mut self) -> Result<Option<T>, DatatrackerError> {
107        match self.iter.next() {
108            Some(x) => {
109                Ok(Some(x))
110            }
111            None => {
112                match self.next.clone() {
113                    Some(ref url_frag) => {
114                        let url = format!("https://datatracker.ietf.org/{}", url_frag);
115                        let mut res = self.dt.connection.get(&url).send()?;
116                        let pl : Page<T> = res.json()?;
117                        self.next = pl.meta.next.clone();
118                        self.iter = pl.objects.into_iter();
119                        self.try_next()
120                    }
121                    None => {
122                        Ok(None)
123                    }
124                }
125            }
126        }
127    }
128}
129
130impl<'a, T> Iterator for PaginatedList<'a, T>
131    where for<'de> T: Deserialize<'de>
132{
133    type Item = Result<T, DatatrackerError>;
134
135    fn next(&mut self) -> Option<Self::Item> {
136        match self.try_next() {
137            Ok(None)    => None,
138            Ok(Some(x)) => Some(Ok(x)),
139            Err(e)      => Some(Err(e))
140        }
141    }
142}
143
144// =================================================================================================================================
145// IETF Datatracker types:
146
147#[derive(Deserialize, Debug, Eq, PartialEq)]
148pub struct EmailUri(String);
149
150/// A mapping from email address to person in the IETF datatracker.
151#[derive(Deserialize, Debug)]
152pub struct Email {
153    pub resource_uri : EmailUri,
154    pub address      : String,
155    pub person       : PersonUri,
156    #[serde(deserialize_with="deserialize_time")]
157    pub time         : DateTime<Utc>,
158    pub origin       : String,
159    pub primary      : bool,
160    pub active       : bool
161}
162
163#[derive(Deserialize, Debug, Eq, PartialEq)]
164pub struct HistoricalEmailUri(String);
165
166#[derive(Deserialize, Debug)]
167pub struct HistoricalEmail {
168    // Fields common with Email:
169    pub resource_uri          : HistoricalEmailUri,
170    pub address               : String,
171    pub person                : PersonUri,
172    #[serde(deserialize_with="deserialize_time")]
173    pub time                  : DateTime<Utc>,
174    pub origin                : String,
175    pub primary               : bool,
176    pub active                : bool,
177    // Fields recording the history:
178    pub history_change_reason : Option<String>,
179    pub history_user          : Option<String>,
180    pub history_id            : u64,
181    pub history_type          : String,
182    #[serde(deserialize_with="deserialize_time")]
183    pub history_date          : DateTime<Utc>
184}
185
186
187#[derive(Deserialize, Debug, Eq, PartialEq)]
188pub struct PersonUri(String);
189
190/// A person in the IETF datatracker.
191#[derive(Deserialize, Debug)]
192#[serde(deny_unknown_fields)]
193pub struct Person {
194    pub id              : u64,
195    pub resource_uri    : PersonUri,
196    pub name            : String,
197    pub name_from_draft : Option<String>,
198    pub biography       : String,
199    pub ascii           : String,
200    pub ascii_short     : Option<String>,
201    #[serde(deserialize_with="deserialize_time")]
202    pub time            : DateTime<Utc>,
203    pub photo           : Option<String>,  // Actually a URL
204    pub photo_thumb     : Option<String>,  // Actually a URL
205    pub user            : Option<String>,
206    pub consent         : Option<bool>
207}
208
209#[derive(Deserialize, Debug, Eq, PartialEq)]
210pub struct HistoricalPersonUri(String);
211
212/// A historical person in the IETF datatracker.
213#[derive(Deserialize, Debug)]
214#[serde(deny_unknown_fields)]
215pub struct HistoricalPerson {
216    // Fields common with Person:
217    pub id                    : u64,
218    pub resource_uri          : HistoricalPersonUri,
219    pub name                  : String,
220    pub name_from_draft       : String,
221    pub biography             : String,
222    pub ascii                 : String,
223    pub ascii_short           : Option<String>,
224    #[serde(deserialize_with="deserialize_time")]
225    pub time                  : DateTime<Utc>,
226    pub photo                 : Option<String>, // Actually a URL
227    pub photo_thumb           : Option<String>, // Actually a URL
228    pub user                  : String,
229    pub consent               : Option<bool>,
230    // Fields recording the history:
231    pub history_change_reason : Option<String>,
232    pub history_user          : String,
233    pub history_type          : String,
234    pub history_id            : u64,
235    #[serde(deserialize_with="deserialize_time")]
236    pub history_date          : DateTime<Utc>,
237}
238
239#[derive(Deserialize, Debug, Eq, PartialEq)]
240pub struct PersonAliasUri(String);
241
242/// An alias in the IETF datatracker.
243#[derive(Deserialize, Debug)]
244#[serde(deny_unknown_fields)]
245pub struct PersonAlias {
246    pub id           : u64,
247    pub resource_uri : PersonAliasUri,
248    pub person       : PersonUri,
249    pub name         : String,
250}
251
252// =================================================================================================================================
253// The DatatrackerError type:
254
255#[derive(Debug)]
256pub enum DatatrackerError {
257    NotFound,
258    IoError(reqwest::Error)
259}
260
261impl fmt::Display for DatatrackerError {
262    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
263        match *self {
264            DatatrackerError::NotFound => write!(f, "Not found"),
265            DatatrackerError::IoError(ref e) => e.fmt(f)
266        }
267    }
268}
269
270impl error::Error for DatatrackerError {
271    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
272        match *self {
273            DatatrackerError::NotFound => None,
274            DatatrackerError::IoError(ref e) => Some(e)
275        }
276    }
277}
278
279impl From<reqwest::Error> for DatatrackerError {
280    fn from(err: reqwest::Error) -> DatatrackerError {
281        DatatrackerError::IoError(err)
282    }
283}
284
285// =================================================================================================================================
286// IETF Datatracker API:
287
288pub struct Datatracker {
289    connection : reqwest::Client
290}
291
292impl Datatracker {
293    fn retrieve<T>(&self, url : &str) -> Result<T, DatatrackerError>
294        where for<'de> T: Deserialize<'de> 
295    {
296        let mut res = self.connection.get(url).send()?;
297        if res.status().is_success() {
298            let res : T = res.json()?;
299            Ok(res)
300        } else {
301            Err(DatatrackerError::NotFound)
302        }
303    }
304
305    pub fn new() -> Self {
306        Datatracker {
307            connection : reqwest::Client::new()
308        }
309    }
310
311    // ----------------------------------------------------------------------------------------------------------------------------
312    // Datatracker API endpoints returning information about email addresses:
313    // * https://datatracker.ietf.org/api/v1/person/email/csp@csperkins.org/
314    // * https://datatracker.ietf.org/api/v1/person/historicalemail/
315
316    /// Retrieve information about an email address.
317    ///
318    /// This returns the information held about a particular email address.
319    /// If you want information about the person with a particular address,
320    /// use `person_from_email()`.
321    pub fn email(&self, email : &str) -> Result<Email, DatatrackerError> {
322        let url = format!("https://datatracker.ietf.org/api/v1/person/email/{}/", email);
323        self.retrieve::<Email>(&url)
324    }
325
326    pub fn email_history_for_address<'a>(&'a self, email : &'a str) -> Result<PaginatedList<HistoricalEmail>, DatatrackerError> {
327        let url = format!("https://datatracker.ietf.org/api/v1/person/historicalemail/?address={}", email);
328        PaginatedList::<'a, HistoricalEmail>::new(self, url)
329    }
330
331    pub fn email_history_for_person<'a>(&'a self, person : &'a Person) -> Result<PaginatedList<HistoricalEmail>, DatatrackerError> {
332        let url = format!("https://datatracker.ietf.org/api/v1/person/historicalemail/?person={}", person.id);
333        PaginatedList::<'a, HistoricalEmail>::new(self, url)
334    }
335
336    // ----------------------------------------------------------------------------------------------------------------------------
337    // Datatracker API endpoints returning information about people:
338    // * https://datatracker.ietf.org/api/v1/person/person/20209/
339    // * https://datatracker.ietf.org/api/v1/person/person/
340    // * https://datatracker.ietf.org/api/v1/person/historicalperson/
341    // * https://datatracker.ietf.org/api/v1/person/alias/
342
343    pub fn person(&self, person_uri : &PersonUri) -> Result<Person, DatatrackerError> {
344        assert!(person_uri.0.starts_with("/api/v1/person/person/"));
345        let url = format!("https://datatracker.ietf.org/{}/", person_uri.0);
346        self.retrieve::<Person>(&url)
347    }
348
349    pub fn person_from_email(&self, email : &str) -> Result<Person, DatatrackerError> {
350        let person = self.email(email)?.person;
351        self.person(&person)
352    }
353
354    pub fn person_aliases<'a>(&'a self, person : &'a Person) -> Result<PaginatedList<PersonAlias>, DatatrackerError> {
355        let url = format!("https://datatracker.ietf.org/api/v1/person/alias/?person={}", person.id);
356        PaginatedList::<'a, PersonAlias>::new(self, url)
357    }
358
359    pub fn person_history<'a>(&'a self, person : &'a Person) -> Result<PaginatedList<HistoricalPerson>, DatatrackerError> {
360        let url = format!("https://datatracker.ietf.org/api/v1/person/historicalperson/?id={}", person.id);
361        PaginatedList::<'a, HistoricalPerson>::new(self, url)
362    }
363
364    pub fn people<'a>(&'a self) -> Result<PaginatedList<'a, Person>, DatatrackerError> {
365        let url = format!("https://datatracker.ietf.org/api/v1/person/person/");
366        PaginatedList::<'a, Person>::new(self, url)
367    }
368
369    pub fn people_with_name<'a>(&'a self, name: &'a str) -> Result<PaginatedList<'a, Person>, DatatrackerError> {
370        let url = format!("https://datatracker.ietf.org/api/v1/person/person/?name={}", name);
371        PaginatedList::<'a, Person>::new(self, url)
372    }
373
374    pub fn people_with_name_containing<'a>(&'a self, name_contains: &'a str) 
375        -> Result<PaginatedList<'a, Person>, DatatrackerError> 
376    {
377        let url = format!("https://datatracker.ietf.org/api/v1/person/person/?name__contains={}", name_contains);
378        PaginatedList::<'a, Person>::new(self, url)
379    }
380
381    pub fn people_between<'a>(&'a self, start: DateTime<Utc>, before: DateTime<Utc>) 
382        -> Result<PaginatedList<'a, Person>, DatatrackerError> 
383    {
384        let s =  start.format("%Y-%m-%dT%H:%M:%S");
385        let b = before.format("%Y-%m-%dT%H:%M:%S");
386        let url = format!("https://datatracker.ietf.org/api/v1/person/person/?time__gte={}&time__lt={}", &s, &b);
387        PaginatedList::<'a, Person>::new(self, url)
388    }
389
390    // ----------------------------------------------------------------------------------------------------------------------------
391    // Datatracker API endpoints returning information about documents:
392    //   https://datatracker.ietf.org/api/v1/doc/document/                        - list of documents
393    //   https://datatracker.ietf.org/api/v1/doc/document/draft-ietf-avt-rtp-new/ - info about document
394    //   https://datatracker.ietf.org/api/v1/doc/docalias/?name=/                 - draft that became the given RFC
395    //   https://datatracker.ietf.org/api/v1/doc/state/                           - Types of state a document can be in
396    //   https://datatracker.ietf.org/api/v1/doc/statetype/                       - Possible types of state for a document
397    //   https://datatracker.ietf.org/api/v1/doc/docevent/                        - list of document events
398    //   https://datatracker.ietf.org/api/v1/doc/docevent/?doc=...                - events for a document
399    //   https://datatracker.ietf.org/api/v1/doc/docevent/?by=...                 - events by a person (as /api/v1/person/person)
400    //   https://datatracker.ietf.org/api/v1/doc/docevent/?time=...               - events by time
401    //   https://datatracker.ietf.org/api/v1/doc/documentauthor/?document=...     - authors of a document
402    //   https://datatracker.ietf.org/api/v1/doc/documentauthor/?person=...       - documents by person (as /api/v1/person/person)
403    //   https://datatracker.ietf.org/api/v1/doc/documentauthor/?email=...        - documents by person with particular email
404    //   https://datatracker.ietf.org/api/v1/doc/dochistory/
405    //   https://datatracker.ietf.org/api/v1/doc/dochistoryauthor/
406    //   https://datatracker.ietf.org/api/v1/doc/docreminder/
407    //   https://datatracker.ietf.org/api/v1/doc/documenturl/
408    //   https://datatracker.ietf.org/api/v1/doc/statedocevent/                   - subset of /api/v1/doc/docevent/; same parameters
409    //   https://datatracker.ietf.org/api/v1/doc/ballotdocevent/                  -               "                "
410    //   https://datatracker.ietf.org/api/v1/doc/newrevisiondocevent/             -               "                "
411    //   https://datatracker.ietf.org/api/v1/doc/submissiondocevent/              -               "                "
412    //   https://datatracker.ietf.org/api/v1/doc/writeupdocevent/                 -               "                "
413    //   https://datatracker.ietf.org/api/v1/doc/consensusdocevent/               -               "                "
414    //   https://datatracker.ietf.org/api/v1/doc/ballotpositiondocevent/          -               "                "
415    //   https://datatracker.ietf.org/api/v1/doc/reviewrequestdocevent/           -               "                "
416    //   https://datatracker.ietf.org/api/v1/doc/lastcalldocevent/                -               "                "
417    //   https://datatracker.ietf.org/api/v1/doc/telechatdocevent/                -               "                "
418    //   https://datatracker.ietf.org/api/v1/doc/relateddocument/?source=...      - documents that source draft relates to (references, replaces, etc)
419    //   https://datatracker.ietf.org/api/v1/doc/relateddocument/?target=...      - documents that relate to target draft
420    //   https://datatracker.ietf.org/api/v1/doc/ballottype/                      - Types of ballot that can be issued on a document
421    //   https://datatracker.ietf.org/api/v1/doc/relateddochistory/
422    //   https://datatracker.ietf.org/api/v1/doc/initialreviewdocevent/
423    //   https://datatracker.ietf.org/api/v1/doc/deletedevent/
424    //   https://datatracker.ietf.org/api/v1/doc/addedmessageevent/
425    //   https://datatracker.ietf.org/api/v1/doc/editedauthorsdocevent/
426
427
428
429    // ----------------------------------------------------------------------------------------------------------------------------
430    // Datatracker API endpoints returning information about names:
431    //   https://datatracker.ietf.org/api/v1/name/doctypename/
432    //   https://datatracker.ietf.org/api/v1/name/streamname/
433    //   https://datatracker.ietf.org/api/v1/name/dbtemplatetypename/
434    //   https://datatracker.ietf.org/api/v1/name/docrelationshipname/
435    //   https://datatracker.ietf.org/api/v1/name/doctagname/
436    //   https://datatracker.ietf.org/api/v1/name/docurltagname/
437    //   https://datatracker.ietf.org/api/v1/name/groupstatename/
438    //   https://datatracker.ietf.org/api/v1/name/formallanguagename/
439    //   https://datatracker.ietf.org/api/v1/name/timeslottypename/
440    //   https://datatracker.ietf.org/api/v1/name/liaisonstatementeventtypename/
441    //   https://datatracker.ietf.org/api/v1/name/stdlevelname/
442    //   https://datatracker.ietf.org/api/v1/name/ballotpositionname/
443    //   https://datatracker.ietf.org/api/v1/name/reviewrequeststatename/
444    //   https://datatracker.ietf.org/api/v1/name/groupmilestonestatename/
445    //   https://datatracker.ietf.org/api/v1/name/iprlicensetypename/
446    //   https://datatracker.ietf.org/api/v1/name/feedbacktypename/
447    //   https://datatracker.ietf.org/api/v1/name/reviewtypename/
448    //   https://datatracker.ietf.org/api/v1/name/iprdisclosurestatename/
449    //   https://datatracker.ietf.org/api/v1/name/reviewresultname/
450    //   https://datatracker.ietf.org/api/v1/name/liaisonstatementstate/
451    //   https://datatracker.ietf.org/api/v1/name/roomresourcename/
452    //   https://datatracker.ietf.org/api/v1/name/liaisonstatementtagname/
453    //   https://datatracker.ietf.org/api/v1/name/topicaudiencename/
454    //   https://datatracker.ietf.org/api/v1/name/continentname/
455    //   https://datatracker.ietf.org/api/v1/name/nomineepositionstatename/
456    //   https://datatracker.ietf.org/api/v1/name/importantdatename/
457    //   https://datatracker.ietf.org/api/v1/name/liaisonstatementpurposename/
458    //   https://datatracker.ietf.org/api/v1/name/constraintname/
459    //   https://datatracker.ietf.org/api/v1/name/sessionstatusname/
460    //   https://datatracker.ietf.org/api/v1/name/ipreventtypename/
461    //   https://datatracker.ietf.org/api/v1/name/agendatypename/
462    //   https://datatracker.ietf.org/api/v1/name/docremindertypename/
463    //   https://datatracker.ietf.org/api/v1/name/intendedstdlevelname/
464    //   https://datatracker.ietf.org/api/v1/name/countryname/
465    //   https://datatracker.ietf.org/api/v1/name/grouptypename/
466    //   https://datatracker.ietf.org/api/v1/name/draftsubmissionstatename/
467    //   https://datatracker.ietf.org/api/v1/name/rolename/
468
469
470
471    // ----------------------------------------------------------------------------------------------------------------------------
472    // Datatracker API endpoints returning information about working groups:
473    //   https://datatracker.ietf.org/api/v1/group/group/                               - list of groups
474    //   https://datatracker.ietf.org/api/v1/group/group/2161/                          - info about group 2161
475    //   https://datatracker.ietf.org/api/v1/group/grouphistory/?group=2161             - history
476    //   https://datatracker.ietf.org/api/v1/group/groupurl/?group=2161                 - URLs
477    //   https://datatracker.ietf.org/api/v1/group/groupevent/?group=2161               - events
478    //   https://datatracker.ietf.org/api/v1/group/groupmilestone/?group=2161           - Current milestones
479    //   https://datatracker.ietf.org/api/v1/group/groupmilestonehistory/?group=2161    - Previous milestones
480    //   https://datatracker.ietf.org/api/v1/group/milestonegroupevent/?group=2161      - changed milestones
481    //   https://datatracker.ietf.org/api/v1/group/role/?group=2161                     - The current WG chairs and ADs of a group
482    //   https://datatracker.ietf.org/api/v1/group/role/?person=20209                   - Groups a person is currently involved with
483    //   https://datatracker.ietf.org/api/v1/group/role/?email=csp@csperkins.org        - Groups a person is currently involved with
484    //   https://datatracker.ietf.org/api/v1/group/rolehistory/?group=2161              - The previous WG chairs and ADs of a group
485    //   https://datatracker.ietf.org/api/v1/group/rolehistory/?person=20209            - Groups person was previously involved with
486    //   https://datatracker.ietf.org/api/v1/group/rolehistory/?email=csp@csperkins.org - Groups person was previously involved with
487    //   https://datatracker.ietf.org/api/v1/group/changestategroupevent/?group=2161    - Group state changes
488    //   https://datatracker.ietf.org/api/v1/group/groupstatetransitions                - ???
489
490
491
492    // ----------------------------------------------------------------------------------------------------------------------------
493    // Datatracker API endpoints returning information about meetings:
494    //   https://datatracker.ietf.org/api/v1/meeting/meeting/                        - list of meetings
495    //   https://datatracker.ietf.org/api/v1/meeting/meeting/747/                    - information about meeting number 747
496    //   https://datatracker.ietf.org/api/v1/meeting/session/                        - list of all sessions in meetings
497    //   https://datatracker.ietf.org/api/v1/meeting/session/25886/                  - a session in a meeting
498    //   https://datatracker.ietf.org/api/v1/meeting/session/?meeting=747            - sessions in meeting number 747
499    //   https://datatracker.ietf.org/api/v1/meeting/session/?meeting=747&group=2161 - sessions in meeting number 747 for group 2161
500    //   https://datatracker.ietf.org/api/v1/meeting/schedtimesessassignment/59003/  - a schededuled session within a meeting
501    //   https://datatracker.ietf.org/api/v1/meeting/timeslot/9480/                  - a time slot within a meeting (time, duration, location)
502    //   https://datatracker.ietf.org/api/v1/meeting/schedule/791/                   - a draft of the meeting agenda
503    //   https://datatracker.ietf.org/api/v1/meeting/room/537/                       - a room at a meeting
504    //   https://datatracker.ietf.org/api/v1/meeting/floorplan/14/                   - floor plan for a meeting venue
505    //   https://datatracker.ietf.org/api/v1/name/meetingtypename/
506
507
508
509}
510// =================================================================================================================================
511// Test suite:
512
513#[cfg(test)]
514mod ietfdata_tests {
515    use super::*;
516
517    #[test]
518    fn test_email() -> Result<(), DatatrackerError> {
519        let dt = Datatracker::new();
520        let e  = dt.email("csp@csperkins.org")?;
521
522        assert_eq!(e.resource_uri, EmailUri("/api/v1/person/email/csp@csperkins.org/".to_string()));
523        assert_eq!(e.address,      "csp@csperkins.org");
524        assert_eq!(e.person,       PersonUri("/api/v1/person/person/20209/".to_string()));
525        assert_eq!(e.time,         Utc.ymd(1970, 1, 1).and_hms(23, 59, 59));
526        assert_eq!(e.origin,       "author: draft-ietf-mmusic-rfc4566bis");
527        assert_eq!(e.primary,      true);
528        assert_eq!(e.active,       true);
529
530        // Lookup a non-existing address; this should fail
531        assert!(dt.email("nobody@example.com").is_err());
532
533        Ok(())
534    }
535
536    #[test]
537    fn test_email_history_for_address() -> Result<(), DatatrackerError> {
538        let dt = Datatracker::new();
539        let h  = dt.email_history_for_address("csp@isi.edu")?.collect::<Result<Vec<_>, _>>()?;
540
541        assert_eq!(h.len(), 1);
542        assert_eq!(h[0].address, "csp@isi.edu");
543        assert_eq!(h[0].person,  PersonUri("/api/v1/person/person/20209/".to_string()));
544
545        Ok(())
546    }
547
548/*
549    #[test]
550    fn test_email_history_for_person() -> Result<(), DatatrackerError> {
551        let dt = Datatracker::new();
552        let p  = dt.person_from_email("csp@csperkins.org")?;
553        for h in dt.email_history_for_person(&p) {
554            println!("{:?}", h);
555        }
556        Ok(())
557    }
558*/
559
560    #[test]
561    fn test_person() -> Result<(), DatatrackerError> {
562        let dt = Datatracker::new();
563        let p  = dt.person(&PersonUri("/api/v1/person/person/20209/".to_string()))?;
564
565        assert_eq!(p.id,              20209);
566        assert_eq!(p.resource_uri,    PersonUri("/api/v1/person/person/20209/".to_string()));
567        assert_eq!(p.name,            "Colin Perkins");
568        assert_eq!(p.name_from_draft, Some("Colin Perkins".to_string()));
569        assert_eq!(p.biography,       "Colin Perkins is a Senior Lecturer (Associate Professor) in the School of Computing Science at the University of Glasgow. His research interests are on transport protocols for real-time and interactive multimedia, and on network protocol design, implementation, and specification. He’s been a participant in the IETF and IRTF since 1996, working primarily in the transport area where he co-chairs the RMCAT working group and is a past chair of the AVT and MMUSIC working groups, and in related IRTF research groups. He proposed and co-chaired the first Applied Networking Research Workshop (ANRW), and has been a long-term participant in the Applied Networking Research Prize (ANRP) awarding committee. He received his BEng in Electronic Engineering in 1992, and my PhD in 1996, both from the Department of Electronics at the University of York.");
570        assert_eq!(p.ascii,           "Colin Perkins");
571        assert_eq!(p.ascii_short,     None);
572        assert_eq!(p.time,            Utc.ymd(2012,2,26).and_hms(0,3,54));
573        assert_eq!(p.photo,           Some("https://www.ietf.org/lib/dt/media/photo/Colin-Perkins-sm.jpg".to_string()));
574        assert_eq!(p.photo_thumb,     Some("https://www.ietf.org/lib/dt/media/photo/Colin-Perkins-sm_PMIAhXi.jpg".to_string()));
575        assert_eq!(p.user,            Some("".to_string()));
576        assert_eq!(p.consent,         Some(true));
577        Ok(())
578    }
579
580    #[test]
581    fn test_person_from_email() -> Result<(), DatatrackerError> {
582        let dt = Datatracker::new();
583        let p  = dt.person_from_email("csp@csperkins.org")?;
584
585        assert_eq!(p.id,   20209);
586        assert_eq!(p.name, "Colin Perkins");
587
588        Ok(())
589    }
590
591/*
592    #[test]
593    fn test_people() {
594        let dt = Datatracker::new();
595        let people = dt.people();
596        for person in people.into_iter() {
597            println!("{:?}", person);
598        }
599    }
600*/
601
602    #[test]
603    fn test_people_with_name() -> Result<(), DatatrackerError> {
604        let dt = Datatracker::new();
605        let people = dt.people_with_name("Colin Perkins")?.collect::<Result<Vec<_>, _>>()?;
606
607        assert_eq!(people[0].id,   20209);
608        assert_eq!(people[0].name, "Colin Perkins");
609
610        Ok(())
611    }
612
613    #[test]
614    fn test_people_with_name_containing() -> Result<(), DatatrackerError> {
615        let dt = Datatracker::new();
616        let people = dt.people_with_name_containing("Perkins")?.collect::<Result<Vec<_>, _>>()?;
617
618        // As of 2019-08-17, there are six people named Perkins in the datatracker.
619        assert_eq!(people.len(), 6);
620
621        Ok(())
622    }
623
624    #[test]
625    fn test_people_between() -> Result<(), DatatrackerError> {
626        let start = Utc.ymd(2019, 7, 1).and_hms( 0,  0,  0);
627        let until = Utc.ymd(2019, 7, 7).and_hms(23, 59, 59);
628
629        let dt = Datatracker::new();
630        let people = dt.people_between(start, until)?.collect::<Result<Vec<_>, _>>()?;
631
632        // There are 26 people in the tracker with dates in the first week of July 2019
633        assert_eq!(people.len(), 26);
634
635        Ok(())
636    }
637
638    #[test]
639    fn test_person_history() -> Result<(), DatatrackerError> {
640        let dt = Datatracker::new();
641        let p  = dt.person_from_email("csp@csperkins.org")?;
642        let h  = dt.person_history(&p)?.collect::<Result<Vec<_>, _>>()?;
643
644        // As of 2019-08-18, there are two history items for csp@csperkins.org
645        assert_eq!(h.len(), 2);
646
647        Ok(())
648    }
649
650    #[test]
651    fn test_person_aliases() -> Result<(), DatatrackerError> {
652        let dt = Datatracker::new();
653        let p  = dt.person_from_email("csp@csperkins.org")?;
654        let h  = dt.person_aliases(&p)?.collect::<Result<Vec<_>, _>>()?;
655
656        // As of 2019-08-18, there are two aliases for csp@csperkins.org
657        assert_eq!(h.len(), 2);
658        assert_eq!(h[0].name, "Dr. Colin Perkins");
659        assert_eq!(h[1].name, "Colin Perkins");
660
661        Ok(())
662    }
663}
664
665// =================================================================================================================================