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