mod abstracts;
mod budget;
mod check;
mod enrich;
mod health;
mod inflight;
mod inputs;
mod lookup;
mod options;
mod registry;
mod resolve;
pub mod retry;
mod search;
mod standard;
pub mod transport;
mod verdict;
mod versions;
pub use abstracts::{Abstract, AbstractsReport};
pub use budget::Budget;
pub use check::{Checked, Finding, Severity, duplicates, findings};
pub use registry::{Disagreement, disagreements};
pub use resolve::{Candidate, ResolveReport, Sighting, Via};
pub use secrecy::SecretString;
#[cfg(feature = "crossref")]
pub use standard::CrossrefConfig;
#[cfg(feature = "datacite")]
pub use standard::DataciteConfig;
#[cfg(feature = "doi")]
pub use standard::DoiConfig;
#[cfg(feature = "europepmc")]
pub use standard::EuropePmcConfig;
#[cfg(feature = "openalex")]
pub use standard::OpenAlexConfig;
#[cfg(feature = "openlibrary")]
pub use standard::OpenLibraryConfig;
pub use standard::{StandardConfig, standard};
pub use verdict::{CitedField, Difference, Reason, Verdict, Verification, describe_update};
pub use versions::VersionsReport;
use std::sync::Arc;
use std::time::Duration;
use aspero_core::{
Error, ErrorCategory, ErrorKind, Provider, ProviderId, ResiliencePolicy, Response, Usage,
};
pub use aspero_store::{Cache, Pacing, SqliteCache, Stats as CacheStats, StoreError};
pub use inputs::{Extras, InputLookup, InputsReport, Looked};
use jiff::Timestamp;
pub use lookup::{Attempt, LookupReport, LookupResult, Outcome};
pub use options::CallOptions;
pub use search::SearchReport;
use crate::budget::Budgets;
use crate::health::Health;
use crate::inflight::InFlight;
use crate::retry::{Jitter, random_jitter, with_retries};
pub type Clock = fn() -> Timestamp;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CachePolicy {
pub records: Duration,
pub misses: Duration,
pub searches: Duration,
}
impl Default for CachePolicy {
fn default() -> Self {
const DAY: u64 = 86_400;
CachePolicy {
records: Duration::from_secs(7 * DAY),
misses: Duration::from_secs(DAY),
searches: Duration::from_secs(DAY),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Served {
Live,
Cache,
Stale,
}
impl Served {
pub fn as_str(self) -> &'static str {
match self {
Served::Live => "live",
Served::Cache => "cache",
Served::Stale => "stale",
}
}
}
#[derive(Clone)]
pub struct Engine {
entries: Vec<Entry>,
jitter: Jitter,
cache: Option<Arc<dyn Cache>>,
cache_policy: CachePolicy,
clock: Clock,
options: CallOptions,
inflight: Arc<InFlight>,
health: Arc<Health>,
budgets: Arc<Budgets>,
}
#[derive(Clone)]
struct Entry {
provider: Arc<dyn Provider>,
policy: ResiliencePolicy,
}
pub struct EngineBuilder {
entries: Vec<(i32, Entry)>,
jitter: Jitter,
cache: Option<Arc<dyn Cache>>,
cache_policy: CachePolicy,
clock: Clock,
}
impl EngineBuilder {
pub fn provider(
mut self,
provider: Arc<dyn Provider>,
policy: ResiliencePolicy,
priority: i32,
) -> Self {
self.entries.push((priority, Entry { provider, policy }));
self
}
pub fn jitter(mut self, jitter: Jitter) -> Self {
self.jitter = jitter;
self
}
pub fn cache(mut self, cache: Arc<dyn Cache>) -> Self {
self.cache = Some(cache);
self
}
pub fn cache_policy(mut self, policy: CachePolicy) -> Self {
self.cache_policy = policy;
self
}
pub fn clock(mut self, clock: Clock) -> Self {
self.clock = clock;
self
}
pub fn build(mut self) -> Engine {
self.entries.sort_by_key(|(priority, _)| *priority);
Engine {
entries: self.entries.into_iter().map(|(_, e)| e).collect(),
jitter: self.jitter,
cache: self.cache,
cache_policy: self.cache_policy,
clock: self.clock,
options: CallOptions::default(),
inflight: Arc::default(),
health: Arc::default(),
budgets: Arc::default(),
}
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum EngineError {
#[error("no enabled provider is named {0:?}")]
UnknownProvider(String),
#[error("no enabled provider supports {0}")]
NoCapableProvider(&'static str),
#[error("{provider}: {error}")]
Provider {
provider: ProviderId,
#[source]
error: Error,
},
}
impl EngineError {
pub fn category(&self) -> ErrorCategory {
match self {
EngineError::UnknownProvider(_) => ErrorCategory::Usage,
EngineError::NoCapableProvider(_) => ErrorCategory::Unsupported,
EngineError::Provider { error, .. } => error.category(),
}
}
pub fn kind(&self) -> ErrorKind {
match self {
EngineError::UnknownProvider(_) => ErrorKind::BadRequest,
EngineError::NoCapableProvider(_) => ErrorKind::Unsupported,
EngineError::Provider { error, .. } => error.kind(),
}
}
}
impl Engine {
pub fn builder() -> EngineBuilder {
EngineBuilder {
entries: Vec::new(),
jitter: random_jitter,
cache: None,
cache_policy: CachePolicy::default(),
clock: Timestamp::now,
}
}
pub fn with_options(&self, options: CallOptions) -> Engine {
Engine {
options,
..self.clone()
}
}
pub fn options(&self) -> CallOptions {
self.options
}
pub fn unavailable(&self) -> Vec<(ProviderId, Error)> {
self.health.unavailable((self.clock)())
}
pub fn budgets(&self) -> Vec<(ProviderId, Budget)> {
self.budgets.all()
}
async fn call<T, F, Fut>(&self, entry: &Entry, op: F) -> Result<Response<T>, Error>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<Response<T>, Error>>,
{
let provider = entry.provider.id();
let pass = self.health.admit(&provider, (self.clock)())?;
let policy = self.options.apply(&entry.policy);
let result = with_retries(&policy, self.jitter, op).await;
let now = (self.clock)();
pass.record(&entry.policy, result.as_ref().err(), now);
let usage = result.as_ref().ok().and_then(|r| r.usage.as_ref());
self.budgets
.note(&provider, usage, result.as_ref().err(), now);
result
}
pub fn providers(&self) -> impl Iterator<Item = &Arc<dyn Provider>> {
self.entries.iter().map(|e| &e.provider)
}
}
fn add_usage(totals: &mut Vec<(ProviderId, Usage)>, provider: &ProviderId, usage: Usage) {
match totals.iter_mut().find(|(p, _)| p == provider) {
Some((_, total)) => *total = total.then(usage),
None => totals.push((provider.clone(), usage)),
}
}