1mod abstracts;
32mod budget;
33mod check;
34mod enrich;
35mod health;
36mod identify;
37mod inflight;
38mod inputs;
39mod lookup;
40mod options;
41mod registry;
42mod resolve;
43pub mod retry;
44mod search;
45mod standard;
46pub mod transport;
47mod verdict;
48mod versions;
49
50pub use abstracts::{Abstract, AbstractsReport};
51pub use budget::Budget;
52pub use check::{Checked, Finding, Severity, duplicates, findings};
53pub use identify::{Decision, Identified, MAX_IDENTIFIERS};
54pub use registry::{Disagreement, disagreements};
55pub use resolve::{Candidate, ResolveReport, Sighting, Via};
56pub use secrecy::SecretString;
58#[cfg(feature = "crossref")]
59pub use standard::CrossrefConfig;
60#[cfg(feature = "datacite")]
61pub use standard::DataciteConfig;
62#[cfg(feature = "doi")]
63pub use standard::DoiConfig;
64#[cfg(feature = "europepmc")]
65pub use standard::EuropePmcConfig;
66#[cfg(feature = "openalex")]
67pub use standard::OpenAlexConfig;
68#[cfg(feature = "openlibrary")]
69pub use standard::OpenLibraryConfig;
70pub use standard::{StandardConfig, standard};
71pub use verdict::{CitedField, Difference, Reason, Verdict, Verification, describe_update};
72pub use versions::VersionsReport;
73
74use std::sync::Arc;
75use std::time::Duration;
76
77use aspero_core::{
78 Error, ErrorCategory, ErrorKind, Provider, ProviderId, ResiliencePolicy, Response, Usage,
79};
80pub use aspero_store::{Cache, Pacing, SqliteCache, Stats as CacheStats, StoreError};
81pub use inputs::{Extras, InputLookup, InputsReport, Looked};
82use jiff::Timestamp;
83pub use lookup::{Attempt, LookupReport, LookupResult, Outcome};
84pub use options::CallOptions;
85pub use search::SearchReport;
86
87use crate::budget::Budgets;
88use crate::health::Health;
89use crate::inflight::InFlight;
90use crate::retry::{Jitter, random_jitter, with_retries};
91
92pub type Clock = fn() -> Timestamp;
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98#[non_exhaustive]
99pub struct CachePolicy {
100 pub records: Duration,
101 pub misses: Duration,
103 pub searches: Duration,
104}
105
106impl Default for CachePolicy {
107 fn default() -> Self {
108 const DAY: u64 = 86_400;
109 CachePolicy {
110 records: Duration::from_secs(7 * DAY),
111 misses: Duration::from_secs(DAY),
112 searches: Duration::from_secs(DAY),
113 }
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119#[non_exhaustive]
120pub enum Served {
121 Live,
122 Cache,
124 Stale,
126}
127
128impl Served {
129 pub fn as_str(self) -> &'static str {
130 match self {
131 Served::Live => "live",
132 Served::Cache => "cache",
133 Served::Stale => "stale",
134 }
135 }
136}
137
138#[derive(Clone)]
142pub struct Engine {
143 entries: Vec<Entry>,
145 jitter: Jitter,
146 cache: Option<Arc<dyn Cache>>,
147 cache_policy: CachePolicy,
148 clock: Clock,
149 options: CallOptions,
150 inflight: Arc<InFlight>,
151 health: Arc<Health>,
152 budgets: Arc<Budgets>,
153}
154
155#[derive(Clone)]
156struct Entry {
157 provider: Arc<dyn Provider>,
158 policy: ResiliencePolicy,
159}
160
161pub struct EngineBuilder {
162 entries: Vec<(i32, Entry)>,
163 jitter: Jitter,
164 cache: Option<Arc<dyn Cache>>,
165 cache_policy: CachePolicy,
166 clock: Clock,
167}
168
169impl EngineBuilder {
170 pub fn provider(
173 mut self,
174 provider: Arc<dyn Provider>,
175 policy: ResiliencePolicy,
176 priority: i32,
177 ) -> Self {
178 self.entries.push((priority, Entry { provider, policy }));
179 self
180 }
181
182 pub fn jitter(mut self, jitter: Jitter) -> Self {
184 self.jitter = jitter;
185 self
186 }
187
188 pub fn cache(mut self, cache: Arc<dyn Cache>) -> Self {
190 self.cache = Some(cache);
191 self
192 }
193
194 pub fn cache_policy(mut self, policy: CachePolicy) -> Self {
195 self.cache_policy = policy;
196 self
197 }
198
199 pub fn clock(mut self, clock: Clock) -> Self {
201 self.clock = clock;
202 self
203 }
204
205 pub fn build(mut self) -> Engine {
206 self.entries.sort_by_key(|(priority, _)| *priority);
207 Engine {
208 entries: self.entries.into_iter().map(|(_, e)| e).collect(),
209 jitter: self.jitter,
210 cache: self.cache,
211 cache_policy: self.cache_policy,
212 clock: self.clock,
213 options: CallOptions::default(),
214 inflight: Arc::default(),
215 health: Arc::default(),
216 budgets: Arc::default(),
217 }
218 }
219}
220
221#[derive(Debug, Clone, thiserror::Error)]
222#[non_exhaustive]
223pub enum EngineError {
224 #[error("no enabled provider is named {0:?}")]
225 UnknownProvider(String),
226 #[error("no enabled provider supports {0}")]
227 NoCapableProvider(&'static str),
228 #[error("{provider}: {error}")]
229 Provider {
230 provider: ProviderId,
231 #[source]
232 error: Error,
233 },
234}
235
236impl EngineError {
237 pub fn category(&self) -> ErrorCategory {
238 match self {
239 EngineError::UnknownProvider(_) => ErrorCategory::Usage,
240 EngineError::NoCapableProvider(_) => ErrorCategory::Unsupported,
241 EngineError::Provider { error, .. } => error.category(),
242 }
243 }
244
245 pub fn kind(&self) -> ErrorKind {
246 match self {
247 EngineError::UnknownProvider(_) => ErrorKind::BadRequest,
248 EngineError::NoCapableProvider(_) => ErrorKind::Unsupported,
249 EngineError::Provider { error, .. } => error.kind(),
250 }
251 }
252}
253
254impl Engine {
255 pub fn builder() -> EngineBuilder {
256 EngineBuilder {
257 entries: Vec::new(),
258 jitter: random_jitter,
259 cache: None,
260 cache_policy: CachePolicy::default(),
261 clock: Timestamp::now,
262 }
263 }
264
265 pub fn with_options(&self, options: CallOptions) -> Engine {
268 Engine {
269 options,
270 ..self.clone()
271 }
272 }
273
274 pub fn options(&self) -> CallOptions {
275 self.options
276 }
277
278 pub fn unavailable(&self) -> Vec<(ProviderId, Error)> {
281 self.health.unavailable((self.clock)())
282 }
283
284 pub fn budgets(&self) -> Vec<(ProviderId, Budget)> {
288 self.budgets.all()
289 }
290
291 async fn call<T, F, Fut>(&self, entry: &Entry, op: F) -> Result<Response<T>, Error>
294 where
295 F: FnMut() -> Fut,
296 Fut: std::future::Future<Output = Result<Response<T>, Error>>,
297 {
298 let provider = entry.provider.id();
299 let pass = self.health.admit(&provider, (self.clock)())?;
300 let policy = self.options.apply(&entry.policy);
301 let result = with_retries(&policy, self.jitter, op).await;
302 let now = (self.clock)();
303 pass.record(&entry.policy, result.as_ref().err(), now);
304 let usage = result.as_ref().ok().and_then(|r| r.usage.as_ref());
305 self.budgets
306 .note(&provider, usage, result.as_ref().err(), now);
307 result
308 }
309
310 pub fn providers(&self) -> impl Iterator<Item = &Arc<dyn Provider>> {
312 self.entries.iter().map(|e| &e.provider)
313 }
314}
315
316fn add_usage(totals: &mut Vec<(ProviderId, Usage)>, provider: &ProviderId, usage: Usage) {
317 match totals.iter_mut().find(|(p, _)| p == provider) {
318 Some((_, total)) => *total = total.then(usage),
319 None => totals.push((provider.clone(), usage)),
320 }
321}