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