Skip to main content

aspero_engine/
standard.rs

1//! Wiring for the built-in providers, so front ends don't repeat it.
2
3use std::sync::Arc;
4
5use aspero_core::{Error, HttpClient, ResiliencePolicy};
6
7use crate::transport::{Paced, ReqwestClient};
8use crate::{Cache, Engine, EngineBuilder, Pacing};
9
10#[cfg(feature = "crossref")]
11pub use aspero_crossref::CrossrefConfig;
12#[cfg(feature = "datacite")]
13pub use aspero_datacite::DataciteConfig;
14#[cfg(feature = "doi")]
15pub use aspero_doi::DoiConfig;
16#[cfg(feature = "europepmc")]
17pub use aspero_europepmc::EuropePmcConfig;
18#[cfg(feature = "openalex")]
19pub use aspero_openalex::OpenAlexConfig;
20#[cfg(feature = "openlibrary")]
21pub use aspero_openlibrary::OpenLibraryConfig;
22
23/// Settings for the compiled-in providers. `None` disables a provider.
24#[derive(Clone, Default)]
25#[non_exhaustive]
26pub struct StandardConfig {
27    /// Contact address sent in the user agent (and as Crossref's `mailto`).
28    pub mailto: Option<String>,
29    /// Where provider answers are cached; `None` for no cache.
30    pub cache: Option<Arc<dyn Cache>>,
31    /// Request pacing shared with other processes (usually the cache
32    /// file); `None` paces this process alone.
33    pub pacing: Option<Arc<dyn Pacing>>,
34    #[cfg(feature = "openalex")]
35    pub openalex: Option<OpenAlexConfig>,
36    #[cfg(feature = "crossref")]
37    pub crossref: Option<CrossrefConfig>,
38    #[cfg(feature = "datacite")]
39    pub datacite: Option<DataciteConfig>,
40    #[cfg(feature = "doi")]
41    pub doi: Option<DoiConfig>,
42    #[cfg(feature = "europepmc")]
43    pub europepmc: Option<EuropePmcConfig>,
44    #[cfg(feature = "openlibrary")]
45    pub openlibrary: Option<OpenLibraryConfig>,
46}
47
48/// Builds an engine with each enabled provider behind its own paced
49/// `reqwest` client.
50///
51/// Priority for lookups: OpenAlex first (free per-ID lookups, many identifier
52/// kinds, indexes DataCite DOIs such as arXiv's), then Crossref, then
53/// DataCite (arXiv, Zenodo, data and software; batched), then doi.org (any
54/// registration agency, one DOI per request; its "not found" is
55/// authoritative), then Europe PMC (PMIDs, PMCIDs, biomedical abstracts).
56/// Open Library answers ISBNs (books) and adds book search.
57pub fn standard(config: StandardConfig) -> Result<Engine, Error> {
58    let user_agent = match &config.mailto {
59        Some(mailto) => format!("aspero/{} (mailto:{mailto})", env!("CARGO_PKG_VERSION")),
60        None => format!("aspero/{}", env!("CARGO_PKG_VERSION")),
61    };
62    // Unused only when every provider feature is off.
63    #[allow(unused_variables)]
64    let client = |label: &str, policy: &ResiliencePolicy| -> Result<Arc<dyn HttpClient>, Error> {
65        let inner = Arc::new(ReqwestClient::new(&user_agent, policy.attempt_timeout)?);
66        let paced =
67            Paced::new(label, inner, policy.min_interval).max_in_flight(policy.max_in_flight);
68        Ok(Arc::new(match &config.pacing {
69            Some(pacing) => paced.shared(pacing.clone()),
70            None => paced,
71        }))
72    };
73
74    let mut builder: EngineBuilder = Engine::builder();
75    if let Some(cache) = config.cache {
76        builder = builder.cache(cache);
77    }
78
79    #[cfg(feature = "openalex")]
80    if let Some(oa) = config.openalex {
81        use aspero_openalex::OpenAlex;
82        let policy = OpenAlex::default_policy();
83        let provider = OpenAlex::new(oa, client("openalex", &policy)?);
84        builder = builder.provider(Arc::new(provider), policy, 10);
85    }
86
87    #[cfg(feature = "crossref")]
88    if let Some(mut cr) = config.crossref {
89        use aspero_crossref::Crossref;
90        if cr.mailto.is_none() {
91            cr.mailto = config.mailto.clone();
92        }
93        let mut policy = Crossref::default_policy();
94        // Crossref answers a second concurrent request from its public
95        // pool with 429; its polite pool (with a contact address) allows
96        // a few.
97        policy.max_in_flight = if cr.mailto.is_some() { 3 } else { 1 };
98        let provider = Crossref::new(cr, client("crossref", &policy)?);
99        builder = builder.provider(Arc::new(provider), policy, 20);
100    }
101
102    #[cfg(feature = "datacite")]
103    if let Some(dc) = config.datacite {
104        use aspero_datacite::Datacite;
105        let policy = Datacite::default_policy();
106        let provider = Datacite::new(dc, client("datacite", &policy)?);
107        builder = builder.provider(Arc::new(provider), policy, 25);
108    }
109
110    #[cfg(feature = "doi")]
111    if let Some(doi) = config.doi {
112        use aspero_doi::DoiOrg;
113        let policy = DoiOrg::default_policy();
114        let provider = DoiOrg::new(doi, client("doi", &policy)?);
115        builder = builder.provider(Arc::new(provider), policy, 30);
116    }
117
118    #[cfg(feature = "europepmc")]
119    if let Some(epmc) = config.europepmc {
120        use aspero_europepmc::EuropePmc;
121        let policy = EuropePmc::default_policy();
122        let provider = EuropePmc::new(epmc, client("europepmc", &policy)?);
123        builder = builder.provider(Arc::new(provider), policy, 35);
124    }
125
126    #[cfg(feature = "openlibrary")]
127    if let Some(ol) = config.openlibrary {
128        use aspero_openlibrary::OpenLibrary;
129        let policy = OpenLibrary::default_policy();
130        let provider = OpenLibrary::new(ol, client("openlibrary", &policy)?);
131        builder = builder.provider(Arc::new(provider), policy, 40);
132    }
133
134    Ok(builder.build())
135}