crossref/
lib.rs

1//! This crate provides a client for interacting with the crossref-api
2//!
3//! [Crossref API docs](https://github.com/CrossRef/rest-api-doc)
4//! `Crossref` - Crossref search API. The `Crossref` crate provides methods matching Crossref API routes:
5
6//! * `works` - `/works` route
7//! * `members` - `/members` route
8//! * `prefixes` - `/prefixes` route
9//! * `funders` - `/funders` route
10//! * `journals` - `/journals` route
11//! * `types` - `/types` route
12//! * `agency` - `/works/{doi}/agency` get DOI minting agency
13//!
14//! ## Usage
15
16//! ### Create a `Crossref` client:
17
18//! ```edition2018
19//! # use crossref::Crossref;
20//! # fn run() -> Result<(), crossref::Error> {
21//! let client = Crossref::builder().build()?;
22//! # Ok(())
23//! # }
24//! ```
25//!
26//! If you have an [Authorization token for Crossref's Plus service](https://github.com/CrossRef/rest-api-doc#authorization-token-for-plus-service):
27//!
28//! ```edition2018
29//! # use crossref::Crossref;
30//! # fn run() -> Result<(), crossref::Error> {
31//! let client = Crossref::builder()
32//! .token("token")
33//! .build()?;
34//! # Ok(())
35//! # }
36//! ```
37//!
38//! Encouraged to use the **The Polite Pool**:
39//!
40//! [Good manners = more reliable service](https://github.com/CrossRef/rest-api-doc#good-manners--more-reliable-service)
41//!
42//! To get into Crossref's polite pool include a email address
43//!
44//! ```edition2018
45//! # use crossref::Crossref;
46//! # fn run() -> Result<(), crossref::Error> {
47//! let client = Crossref::builder()
48//!     .polite("polite@example.com")
49//!     .token("your token")
50//!     .build()?;
51//! # Ok(())
52//! # }
53//! ```
54//!
55//! ### Constructing Queries
56//! Not all components support queries and there are custom available parameters for each route that supports querying.
57//! For each resource components that supports querying there exist a Query struct: `WorksQuery`, `MembersQuery`, `FundersQuery`. The `WorksQuery` also differs from the others by supporting [deep paging with cursors](https://github.com/CrossRef/rest-api-doc#deep-paging-with-cursors) and [field queries](https://github.com/CrossRef/rest-api-doc#works-field-queries).
58//!
59//! Otherwise creating queries works the same for all resource components:
60//!
61//! ```edition2018
62//! # use crossref::*;
63//! # fn run() -> Result<()> {
64//! let query = WorksQuery::new("Machine Learning")
65//! // field queries supported for `Works`
66//! .field_query(FieldQuery::author("Some Author"))
67//! // filters are specific for each resource component
68//! .filter(WorksFilter::HasOrcid)
69//! .order(Order::Asc)
70//! .sort(Sort::Score);
71//! # Ok(())
72//! # }
73//! ```
74//!
75//!
76//! ### Get Records
77//!
78//! See [this table](https://github.com/CrossRef/rest-api-doc#resource-components) for a detailed overview of the major components.
79//!
80//! There are 3 available targets:
81//!
82//! * **standalone resource components**: `/works`, `/members`, etc. that return a list list of the corresponding items and can be specified with queries
83//! * **Resource component with identifiers**: `/works/{doi}?<query>`,`/members/{member_id}?<query>`, etc. that returns a single item if found.
84//! * **combined with the `works` route**: The works component can be appended to other resources: `/members/{member_id}/works?<query>` etc. that returns a list of matching `Work` items.
85//!
86//! This resembles in the enums of the resource components, eg. for `Members`:
87//!
88//! ```edition2018
89//! # use crossref::query::*;
90//! pub enum Members {
91//!     /// target a specific member at `/members/{id}`
92//!     Identifier(String),
93//!     /// target all members that match the query at `/members?query...`
94//!     Query(MembersQuery),
95//!     /// target a `Work` for a specific member at `/members/{id}/works?query..`
96//!     Works(WorksIdentQuery),
97//! }
98//! ```
99//!
100//! All options are supported by the client:
101//!
102//! **Single Item by DOI (ID)**
103//!
104//! Analogous methods exist for all resource components
105//!
106//! ```edition2018
107//! # use crossref::*;
108//! # fn run() -> Result<()> {
109//! # let client = Crossref::builder().build()?;
110//! let work = client.work("10.1037/0003-066X.59.1.29")?;
111//!
112//! let agency = client.work_agency("10.1037/0003-066X.59.1.29")?;
113//!
114//! let funder = client.funder("funder_id")?;
115//!
116//! let member = client.member("member_id")?;
117//! # Ok(())
118//! # }
119//! ```
120//!
121//! **Query**
122//!
123//! ```edition2018
124//! # use crossref::*;
125//! # fn run() -> Result<()> {
126//! # let client = Crossref::builder().build()?;
127//! let query = WorksQuery::new("Machine Learning");
128//!
129//! // one page of the matching results
130//! let works = client.works(query)?;
131//! # Ok(())
132//! # }
133//! ```
134//!
135//! Alternatively insert a free form query term directly
136//!
137//! ```edition2018
138//! # use crossref::*;
139//! # fn run() -> Result<()> {
140//! # let client = Crossref::builder().build()?;
141//!
142//! // one page of the matching results
143//! let works = client.works("Machine Learning")?;
144//! # Ok(())
145//! # }
146//! ```
147//!
148//! **Combining Routes with the `Works` route**
149//!
150//! For each resource component other than `Works` there exist methods to append a `WorksQuery` with the ID option `/members/{member_id}/works?<query>?`
151//!
152//! ```edition2018
153//! # use crossref::*;
154//! # fn run() -> Result<()> {
155//! # let client = Crossref::builder().build()?;
156//! let works = client.member_works( WorksQuery::new("machine learning")
157//! .sort(Sort::Score).into_ident("member_id"))?;
158//! # Ok(())
159//! # }
160//! ```
161//!
162//! This would be the same as using the [`Crossref::works`] method by supplying the combined type
163//!
164//! ```edition2018
165//! # use crossref::*;
166//! # fn run() -> Result<()> {
167//! # let client = Crossref::builder().build()?;
168//! let works = client.works(WorksQuery::new("machine learning")
169//!     .sort(Sort::Score)
170//!     .into_combined_query::<Members>("member_id"))?;
171//! # Ok(())
172//! # }
173//! ```
174//!
175//! ** Deep paging for `Works` **
176//! [Deep paging results](https://github.com/CrossRef/rest-api-doc#deep-paging-with-cursors)
177//! Deep paging is supported for all queries, that return a list of `Work`, `WorkList`.
178//! This function returns a new iterator over pages of `Work`, which is returned as bulk of items as a `WorkList` by crossref.
179//! Usually a single page `WorkList` contains 20 items.
180//!
181//! # Example
182//!
183//! Iterate over all `Works` linked to search term `Machine Learning`
184//!
185//! ```edition2018
186//! use crossref::{Crossref, WorksQuery, Work};
187//! # fn run() -> Result<(), crossref::Error> {
188//! let client = Crossref::builder().build()?;
189//!
190//! let all_works: Vec<Work> = client.deep_page(WorksQuery::new("Machine Learning")).flat_map(|x|x.items).collect();
191//!
192//! # Ok(())
193//! # }
194//! ```
195//!
196//! Which can be simplified to
197//!
198//! ```edition2018
199//! use crossref::{Crossref, WorksQuery, Work};
200//! # fn run() -> Result<(), crossref::Error> {
201//! let client = Crossref::builder().build()?;
202//!
203//! let all_works: Vec<Work> = client.deep_page("Machine Learning").into_work_iter().collect();
204//!
205//! # Ok(())
206//! # }
207//! ```
208//!
209//!
210//! # Example
211//!
212//! Iterate over all the pages (`WorkList`) of the funder with id `funder id` by using a combined query.
213//! A single `WorkList` usually holds 20 `Work` items.
214//!
215//! ```edition2018
216//! use crossref::{Crossref, Funders, WorksQuery, Work, WorkList};
217//! # fn run() -> Result<(), crossref::Error> {
218//! let client = Crossref::builder().build()?;
219//!
220//! let all_funder_work_list: Vec<WorkList> = client.deep_page(WorksQuery::default().into_combined_query::<Funders>("funder id")).collect();
221//!
222//! # Ok(())
223//! # }
224//! ```
225//! # Example
226//!
227//! Iterate over all `Work` items of a specfic funder directly.
228//!
229//! ```edition2018
230//! use crossref::{Crossref, Funders, WorksQuery, Work, WorkList};
231//! # fn run() -> Result<(), crossref::Error> {
232//! let client = Crossref::builder().build()?;
233//!
234//! let all_works: Vec<Work> = client.deep_page(WorksQuery::default()
235//!         .into_combined_query::<Funders>("funder id"))
236//!         .into_work_iter()
237//!         .collect();
238//!
239//! # Ok(())
240//! # }
241//! ```
242
243#![deny(warnings)]
244#![deny(missing_docs)]
245#![allow(unused)]
246
247mod error;
248/// provides types to construct a specific query
249pub mod query;
250/// provides the response types of the crossref api
251pub mod response;
252
253// TODO extract to optional feature?
254/// content negotiation
255pub mod cn;
256/// textual data mining
257pub mod tdm;
258
259#[doc(inline)]
260pub use self::error::{Error, Result};
261
262#[doc(inline)]
263pub use self::query::works::{
264    FieldQuery, WorkListQuery, WorkResultControl, Works, WorksFilter, WorksIdentQuery, WorksQuery,
265};
266
267#[doc(inline)]
268pub use self::query::{Component, CrossrefQuery, CrossrefRoute, Order, Sort};
269pub use self::query::{Funders, Journals, Members, Prefixes, Type, Types};
270pub use self::response::{
271    CrossrefType, Funder, FunderList, Journal, JournalList, Member, MemberList, TypeList, Work,
272    WorkAgency, WorkList,
273};
274
275pub(crate) use self::response::{Message, Response};
276
277use crate::error::ErrorKind;
278use crate::query::{FundersQuery, MembersQuery, ResourceComponent};
279use crate::response::{MessageType, Prefix};
280use reqwest::{self, Client};
281use std::iter::FlatMap;
282use std::rc::Rc;
283
284macro_rules! get_item {
285    ($ident:ident, $value:expr, $got:expr) => {
286        if let Some(msg) = $value {
287            match msg {
288                Message::$ident(item) => Ok(item),
289                _ => Err(ErrorKind::UnexpectedItem {
290                    expected: MessageType::$ident,
291                    got: $got,
292                }
293                .into()),
294            }
295        } else {
296            Err(ErrorKind::MissingMessage {
297                expected: MessageType::$ident,
298            }
299            .into())
300        }
301    };
302}
303
304macro_rules! impl_combined_works_query {
305    ($($name:ident  $component:ident,)*) => {
306        $(
307        /// Return one page of the components's `Work` that match the query
308        ///
309        pub fn $name(&self, ident: WorksIdentQuery) -> Result<WorkList> {
310            let resp = self.get_response(&$component::Works(ident))?;
311            get_item!(WorkList, resp.message, resp.message_type)
312        })+
313    };
314}
315
316/// Struct for Crossref search API methods
317#[derive(Debug, Clone)]
318pub struct Crossref {
319    /// use another base url than `api.crossref.org`
320    pub base_url: String,
321    /// the reqwest client that handles the requests
322    pub client: Rc<Client>,
323}
324
325impl Crossref {
326    const BASE_URL: &'static str = "https://api.crossref.org";
327
328    /// Constructs a new `CrossrefBuilder`.
329    ///
330    /// This is the same as `Crossref::builder()`.
331    pub fn builder() -> CrossrefBuilder {
332        CrossrefBuilder::new()
333    }
334
335    // generate all functions to query combined endpoints
336    impl_combined_works_query!(funder_works Funders, member_works Members,
337    type_works Types, journal_works Journals, prefix_works Prefixes,);
338
339    /// Transforms the `CrossrefQuery` in the request route and  executes the request
340    ///
341    /// # Errors
342    ///
343    /// If it was a bad url, the server will return `Resource not found` a `ResourceNotFound` error will be returned in this case
344    /// Also fails if the json response body could be parsed into `Response`
345    /// Fails if there was an error in reqwest executing the request [::reqwest::RequestBuilder::send]
346    fn get_response<T: CrossrefQuery>(&self, query: &T) -> Result<Response> {
347        let resp = self
348            .client
349            .get(&query.to_url(&self.base_url)?)
350            .send()?
351            .text()?;
352        if resp.starts_with("Resource not found") {
353            Err(ErrorKind::ResourceNotFound {
354                resource: Box::new(query.clone().resource_component()),
355            }
356            .into())
357        } else {
358            Ok(serde_json::from_str(&resp)?)
359        }
360    }
361
362    /// Return the `Work` items that match a certain query.
363    ///
364    /// To search only by query terms use the convenience query method [Crossref::query_works]
365    ///
366    /// # Example
367    ///
368    /// ```edition2018
369    /// use crossref::{Crossref, WorksQuery, WorksFilter, FieldQuery};
370    /// # fn run() -> Result<(), crossref::Error> {
371    /// let client = Crossref::builder().build()?;
372    ///
373    /// let query = WorksQuery::new("Machine Learning")
374    ///     .filter(WorksFilter::HasOrcid)
375    ///     .order(crossref::Order::Asc)
376    ///     .field_query(FieldQuery::author("Some Author"))
377    ///     .sort(crossref::Sort::Score);
378    ///
379    /// let works = client.works(query)?;
380    ///
381    /// # Ok(())
382    /// # }
383    /// ```
384    ///
385    /// # Errors
386    ///
387    /// This method fails if the `works` element expands to a bad route `ResourceNotFound`
388    /// Fails if the response body doesn't have `message` field `MissingMessage`.
389    /// Fails if anything else than a `WorkList` is returned as message `UnexpectedItem`
390    pub fn works<T: Into<WorkListQuery>>(&self, query: T) -> Result<WorkList> {
391        let resp = self.get_response(&query.into())?;
392        get_item!(WorkList, resp.message, resp.message_type)
393    }
394
395    /// Return the `Work` that is identified by  the `doi`.
396    ///
397    /// # Errors
398    /// This method fails if the doi could not identified `ResourceNotFound`
399    ///
400    pub fn work(&self, doi: &str) -> Result<Work> {
401        let resp = self.get_response(&Works::Identifier(doi.to_string()))?;
402        get_item!(Work, resp.message, resp.message_type).map(|x| *x)
403    }
404
405    /// [Deep paging results](https://github.com/CrossRef/rest-api-doc#deep-paging-with-cursors)
406    /// Deep paging is supported for all queries, that return a list of `Work`, `WorkList`.
407    /// This function returns a new iterator over pages of `Work`, which is returned as bulk of items as a `WorkList` by crossref.
408    /// Usually a single page `WorkList` contains 20 items.
409    ///
410    /// # Example
411    ///
412    /// Iterate over all `Works` linked to search term `Machine Learning`
413    ///
414    /// ```edition2018
415    /// use crossref::{Crossref, WorksQuery, Work};
416    /// # fn run() -> Result<(), crossref::Error> {
417    /// let client = Crossref::builder().build()?;
418    ///
419    /// let all_works: Vec<Work> = client.deep_page(WorksQuery::new("Machine Learning")).flat_map(|x|x.items).collect();
420    ///
421    /// # Ok(())
422    /// # }
423    /// ```
424    ///
425    /// # Example
426    ///
427    /// Iterate over all the pages (`WorkList`) of the funder with id `funder id` by using a combined query.
428    /// A single `WorkList` usually holds 20 `Work` items.
429    ///
430    /// ```edition2018
431    /// use crossref::{Crossref, Funders, WorksQuery, Work, WorkList};
432    /// # fn run() -> Result<(), crossref::Error> {
433    /// let client = Crossref::builder().build()?;
434    ///
435    /// let all_funder_work_list: Vec<WorkList> = client.deep_page(WorksQuery::default().into_combined_query::<Funders>("funder id")).collect();
436    ///
437    /// # Ok(())
438    /// # }
439    /// ```
440    /// # Example
441    ///
442    /// Iterate over all `Work` items of a specfic funder directly.
443    ///
444    /// ```edition2018
445    /// use crossref::{Crossref, Funders, WorksQuery, Work, WorkList};
446    /// # fn run() -> Result<(), crossref::Error> {
447    /// let client = Crossref::builder().build()?;
448    ///
449    /// let all_works: Vec<Work> = client.deep_page(WorksQuery::default()
450    ///         .into_combined_query::<Funders>("funder id"))
451    ///         .into_work_iter()
452    ///         .collect();
453    ///
454    /// # Ok(())
455    /// # }
456    /// ```
457    ///
458    /// # Example
459    ///
460    /// Alternatively deep page without an iterator by handling the cursor directly
461    ///
462    /// ```edition2018
463    /// use crossref::{Crossref, WorksQuery, WorksFilter};
464    /// # fn run() -> Result<(), crossref::Error> {
465    /// let client = Crossref::builder().build()?;
466    ///
467    /// // request a next-cursor first
468    /// let query = WorksQuery::new("Machine Learning")
469    ///     .new_cursor();
470    ///
471    /// let works = client.works(query.clone())?;
472    ///
473    /// // this continues from where this first response stopped
474    /// // if no more work items are available then a empty list will be returned
475    /// let deep_works = client.works(
476    ///     query.next_cursor(&works.next_cursor.unwrap())
477    /// )?;
478    /// # Ok(())
479    /// # }
480    /// ```
481    ///
482    pub fn deep_page<T: Into<WorkListQuery>>(&self, query: T) -> WorkListIterator {
483        WorkListIterator {
484            query: query.into(),
485            client: self,
486            index: 0,
487            finish_next_iteration: false,
488        }
489    }
490
491    /// Return the `Agency` that registers the `Work` identified by  the `doi`.
492    ///
493    /// # Errors
494    /// This method fails if the doi could not identified `ResourceNotFound`
495    ///
496    pub fn work_agency(&self, doi: &str) -> Result<WorkAgency> {
497        let resp = self.get_response(&Works::Agency(doi.to_string()))?;
498        get_item!(WorkAgency, resp.message, resp.message_type)
499    }
500
501    /// Return the matching `Funders` items.
502    pub fn funders(&self, funders: FundersQuery) -> Result<FunderList> {
503        let resp = self.get_response(&Funders::Query(funders))?;
504        get_item!(FunderList, resp.message, resp.message_type)
505    }
506
507    /// Return the `Funder` for the `id`
508    pub fn funder(&self, id: &str) -> Result<Funder> {
509        let resp = self.get_response(&Funders::Identifier(id.to_string()))?;
510        get_item!(Funder, resp.message, resp.message_type).map(|x| *x)
511    }
512
513    /// Return the matching `Members` items.
514    pub fn members(&self, members: MembersQuery) -> Result<MemberList> {
515        let resp = self.get_response(&Members::Query(members))?;
516        get_item!(MemberList, resp.message, resp.message_type)
517    }
518
519    /// Return the `Member` for the `id`
520    pub fn member(&self, member_id: &str) -> Result<Member> {
521        let resp = self.get_response(&Members::Identifier(member_id.to_string()))?;
522        get_item!(Member, resp.message, resp.message_type).map(|x| *x)
523    }
524
525    /// Return the `Prefix` for the `id`
526    pub fn prefix(&self, id: &str) -> Result<Prefix> {
527        let resp = self.get_response(&Prefixes::Identifier(id.to_string()))?;
528        get_item!(Prefix, resp.message, resp.message_type)
529    }
530    /// Return a specific `Journal`
531    pub fn journal(&self, id: &str) -> Result<Journal> {
532        let resp = self.get_response(&Journals::Identifier(id.to_string()))?;
533        get_item!(Journal, resp.message, resp.message_type).map(|x| *x)
534    }
535
536    /// Return all available `Type`
537    pub fn types(&self) -> Result<TypeList> {
538        let resp = self.get_response(&Types::All)?;
539        get_item!(TypeList, resp.message, resp.message_type)
540    }
541
542    /// Return the `Type` for the `id`
543    pub fn type_(&self, id: &Type) -> Result<CrossrefType> {
544        let resp = self.get_response(&Types::Identifier(id.id().to_string()))?;
545        get_item!(Type, resp.message, resp.message_type)
546    }
547
548    /// Get a random set of DOIs
549    ///
550    /// # Example
551    ///
552    /// ```edition2018
553    /// use crossref::Crossref;
554    /// # fn run() -> Result<(), crossref::Error> {
555    /// # let client = Crossref::builder().build()?;
556    /// // this will return 10 random dois from the crossref api
557    /// let random_dois = client.random_dois(10)?;
558    /// # Ok(())
559    /// # }
560    /// ```
561    pub fn random_dois(&self, len: usize) -> Result<Vec<String>> {
562        self.works(WorksQuery::random(len))
563            .map(|x| x.items.into_iter().map(|x| x.doi).collect())
564    }
565}
566
567/// A `CrossrefBuilder` can be used to create `Crossref` with additional config.
568///
569/// # Example
570///
571/// ```edition2018
572/// use crossref::Crossref;
573/// # fn run() -> Result<(), crossref::Error> {
574///
575/// let client = Crossref::builder()
576///     .polite("polite@example.com")
577///     .token("your token")
578///     .build()?;
579/// # Ok(())
580/// # }
581/// ```
582#[derive(Default)]
583pub struct CrossrefBuilder {
584    /// [Good manners = more reliable service.](https://github.com/CrossRef/rest-api-doc#good-manners--more-reliable-service)
585    ///
586    /// will add a `User-Agent` header by default with with the `email` email.
587    /// crossref can contact you if your script misbehaves
588    /// this will get you directed to the "polite pool"
589    user_agent: Option<String>,
590    /// the token for the Crossref Plus service will be included as `Authorization` header
591    /// This token will ensure that said requests get directed to a pool of machines that are reserved for "Plus" SLA users.
592    plus_token: Option<String>,
593    /// use a different base url than `Crossref::BASE_URL` https://api.crossref.org
594    base_url: Option<String>,
595}
596
597impl CrossrefBuilder {
598    /// Constructs a new `CrossrefBuilder`.
599    ///
600    /// This is the same as `Crossref::builder()`.
601    pub fn new() -> CrossrefBuilder {
602        CrossrefBuilder::default()
603    }
604
605    /// be polite and set your email as `User-Agent`
606    /// will get you in the polite pool of crossref
607    pub fn polite(mut self, email: &str) -> Self {
608        self.user_agent = Some(format!("mailto:{}", email));
609        self
610    }
611
612    /// set the user agent directly
613    pub fn user_agent(mut self, user_agent: &str) -> Self {
614        self.user_agent = Some(user_agent.to_string());
615        self
616    }
617
618    /// set a crossref plus service  API token
619    pub fn token(mut self, token: &str) -> Self {
620        self.plus_token = Some(token.to_string());
621        self
622    }
623
624    /// Returns a `Crossref` that uses this `CrossrefBuilder` configuration.
625    /// # Errors
626    ///
627    /// This will fail if TLS backend cannot be initialized see [reqwest::ClientBuilder::build]
628    pub fn build(self) -> Result<Crossref> {
629        use reqwest::header;
630        let mut headers = header::HeaderMap::new();
631        if let Some(agent) = &self.user_agent {
632            headers.insert(
633                header::USER_AGENT,
634                header::HeaderValue::from_str(agent).map_err(|_| ErrorKind::Config {
635                    msg: format!("failed to create User Agent header for `{}`", agent),
636                })?,
637            );
638        }
639        if let Some(token) = &self.plus_token {
640            headers.insert(
641                header::AUTHORIZATION,
642                header::HeaderValue::from_str(token).map_err(|_| ErrorKind::Config {
643                    msg: format!("failed to create AUTHORIZATION header for `{}`", token),
644                })?,
645            );
646        }
647        let client = reqwest::Client::builder()
648            .default_headers(headers)
649            .build()
650            .map_err(|_| ErrorKind::Config {
651                msg: "failed to initialize TLS backend".to_string(),
652            })?;
653
654        Ok(Crossref {
655            base_url: self
656                .base_url
657                .unwrap_or_else(|| Crossref::BASE_URL.to_string()),
658            client: Rc::new(client),
659        })
660    }
661}
662
663/// Allows iterating of deep page work request
664pub struct WorkListIterator<'a> {
665    /// the query
666    query: WorkListQuery,
667    /// performs each request
668    client: &'a Crossref,
669    /// stores how many results already retrieved
670    index: usize,
671    /// whether the iterator should finish next iteration
672    finish_next_iteration: bool,
673}
674impl<'a> WorkListIterator<'a> {
675    /// convenience method to create a `WorkIterator`
676    pub fn into_work_iter(self) -> impl Iterator<Item = Work> + 'a {
677        self.flat_map(|x| x.items)
678    }
679}
680
681impl<'a> Iterator for WorkListIterator<'a> {
682    type Item = WorkList;
683
684    fn next(&mut self) -> Option<Self::Item> {
685        if self.finish_next_iteration {
686            return None;
687        }
688
689        {
690            let control = &mut self.query.query_mut().result_control;
691
692            // if no result control is set, set a new cursor
693            if control.is_none() {
694                *control = Some(WorkResultControl::new_cursor());
695            }
696        }
697
698        let resp = self.client.get_response(&self.query);
699        if let Ok(resp) = resp {
700            let worklist: Result<WorkList> = get_item!(WorkList, resp.message, resp.message_type);
701            if let Ok(worklist) = worklist {
702                if let Some(cursor) = &worklist.next_cursor {
703                    match &mut self.query.query_mut().result_control {
704                        Some(WorkResultControl::Cursor { token, .. }) => {
705                            // use the received cursor token in next iteration
706                            *token = Some(cursor.clone())
707                        }
708                        Some(WorkResultControl::Standard(_)) => {
709                            // standard result control was set, don't deep page and return next iteration
710                            self.finish_next_iteration = true;
711                        }
712                        _ => (),
713                    }
714                } else {
715                    // no cursor received, end next iteration
716                    self.finish_next_iteration = true;
717                }
718
719                if worklist.items.is_empty() {
720                    None
721                } else {
722                    Some(worklist)
723                }
724            } else {
725                // failed to deserialize response into `WorkList`
726                None
727            }
728        } else {
729            // no response received
730            None
731        }
732    }
733}