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