Skip to main content

aspero_engine/
inputs.rs

1//! Looking up identifiers as people and documents write them: parsing each,
2//! retrying DOIs whose trailing punctuation was part of them, and adding
3//! what a full answer needs. Front ends share this so they answer alike.
4
5use aspero_core::{Doi, FieldSet, IdError, ProviderId, SourceRecord, Usage, WorkId};
6
7use crate::enrich::found_records;
8use crate::{Abstract, Disagreement, Engine, LookupReport, LookupResult, add_usage};
9
10/// What to add to each record found.
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
12#[non_exhaustive]
13pub struct Extras {
14    /// Notices in both directions, dated; open access from whichever
15    /// provider knows; disagreements with the DOI's registry; and the
16    /// latest version of superseded works.
17    pub full: bool,
18    pub abstracts: bool,
19}
20
21impl Extras {
22    pub fn full() -> Self {
23        Extras {
24            full: true,
25            abstracts: false,
26        }
27    }
28}
29
30/// One input's lookup.
31#[derive(Debug, Clone)]
32pub enum InputLookup {
33    /// Not an identifier.
34    Invalid(IdError),
35    Looked(Box<Looked>),
36}
37
38#[derive(Debug, Clone)]
39#[non_exhaustive]
40pub struct Looked {
41    pub result: LookupResult,
42    /// Found only with the trailing punctuation kept.
43    pub with_punctuation: bool,
44    /// With `full`: where the record contradicts the DOI's registry.
45    pub disagreements: Vec<Disagreement>,
46    /// With `full`: the latest version, if the work was superseded.
47    pub latest: Option<SourceRecord>,
48    /// With `abstracts`, for works found: the abstract, if any provider
49    /// has one.
50    pub abstract_text: Option<Option<Abstract>>,
51}
52
53#[derive(Debug, Clone)]
54pub struct InputsReport {
55    /// One per input, in order.
56    pub items: Vec<InputLookup>,
57    pub usage: Vec<(ProviderId, Usage)>,
58}
59
60impl Engine {
61    /// Looks up each input as written: one batched lookup, a second for
62    /// DOIs not found unless their trailing punctuation is kept, then
63    /// `extras`. Never fails as a whole.
64    pub async fn lookup_inputs(&self, inputs: &[String], extras: Extras) -> InputsReport {
65        let parsed: Vec<Result<WorkId, IdError>> = inputs.iter().map(|s| s.parse()).collect();
66        let (positions, valid): (Vec<usize>, Vec<WorkId>) = parsed
67            .iter()
68            .enumerate()
69            .filter_map(|(i, p)| Some((i, p.as_ref().ok()?.clone())))
70            .unzip();
71        let mut report = self.lookup(&valid, FieldSet::DEFAULT).await;
72        let retried = self
73            .retry_with_punctuation(inputs, &positions, &mut report)
74            .await;
75
76        let n = report.results.len();
77        let mut disagreements = vec![Vec::new(); n];
78        let mut latest: Vec<Option<SourceRecord>> = vec![None; n];
79        let mut abstracts: Vec<Option<Option<Abstract>>> = vec![None; n];
80        if extras.full {
81            self.add_notices(&mut report).await;
82            self.add_open_access(&mut report).await;
83            disagreements = self.cross_check(&mut report).await;
84            let found: Vec<(usize, &SourceRecord)> =
85                found_records(&report.results, |_| true).collect();
86            let records: Vec<&SourceRecord> = found.iter().map(|(_, r)| *r).collect();
87            let versions = self.latest_versions(&records).await;
88            for ((i, _), record) in found.iter().zip(versions.latest) {
89                latest[*i] = record;
90            }
91            for (p, u) in versions.usage {
92                add_usage(&mut report.usage, &p, u);
93            }
94        }
95        if extras.abstracts {
96            let found: Vec<(usize, &SourceRecord)> =
97                found_records(&report.results, |_| true).collect();
98            let records: Vec<&SourceRecord> = found.iter().map(|(_, r)| *r).collect();
99            let fetched = self.abstracts(&records).await;
100            for ((i, _), a) in found.iter().zip(fetched.found) {
101                abstracts[*i] = Some(a);
102            }
103            for (p, u) in fetched.usage {
104                add_usage(&mut report.usage, &p, u);
105            }
106        }
107
108        let mut looked = report
109            .results
110            .into_iter()
111            .zip(disagreements)
112            .zip(latest)
113            .zip(abstracts)
114            .map(
115                |(((result, disagreements), latest), abstract_text)| Looked {
116                    with_punctuation: retried.contains(&result.id),
117                    result,
118                    disagreements,
119                    latest,
120                    abstract_text,
121                },
122            );
123        let items = parsed
124            .into_iter()
125            .map(|p| match p {
126                Err(e) => InputLookup::Invalid(e),
127                Ok(_) => InputLookup::Looked(Box::new(
128                    looked.next().expect("one result per valid input"),
129                )),
130            })
131            .collect();
132        InputsReport {
133            items,
134            usage: report.usage,
135        }
136    }
137
138    /// DOIs may legally end in `.` (or `,;:`), which parsing strips as
139    /// sentence punctuation. For DOIs not found that way, asks once more
140    /// with the punctuation kept, in one batch; found ones replace their
141    /// results. Returns the identifiers found only on retry.
142    async fn retry_with_punctuation(
143        &self,
144        inputs: &[String],
145        positions: &[usize],
146        report: &mut LookupReport,
147    ) -> Vec<WorkId> {
148        let retries: Vec<(usize, WorkId)> = positions
149            .iter()
150            .zip(&report.results)
151            .enumerate()
152            .filter(|(_, (_, r))| r.is_not_found())
153            .filter_map(|(i, (&at, _))| {
154                Some((i, WorkId::Doi(Doi::with_trailing_punctuation(&inputs[at])?)))
155            })
156            .collect();
157        if retries.is_empty() {
158            return Vec::new();
159        }
160        let ids: Vec<WorkId> = retries.iter().map(|(_, id)| id.clone()).collect();
161        let second = self.lookup(&ids, FieldSet::DEFAULT).await;
162        for (p, u) in second.usage {
163            add_usage(&mut report.usage, &p, u);
164        }
165        let mut found = Vec::new();
166        for ((i, _), result) in retries.into_iter().zip(second.results) {
167            if result.record.is_some() {
168                found.push(result.id.clone());
169                report.results[i] = result;
170            }
171        }
172        found
173    }
174}