#[cfg(feature = "source-acm")]
mod acm;
#[cfg(feature = "source-arxiv")]
mod arxiv;
#[cfg(feature = "source-base")]
mod base;
#[cfg(feature = "source-biorxiv")]
mod biorxiv;
#[cfg(feature = "source-connected_papers")]
mod connected_papers;
#[cfg(feature = "source-core-repo")]
mod core;
#[cfg(feature = "source-crossref")]
mod crossref;
#[cfg(feature = "source-dblp")]
mod dblp;
#[cfg(feature = "source-dimensions")]
mod dimensions;
#[cfg(feature = "source-doaj")]
mod doaj;
#[cfg(feature = "source-europe_pmc")]
mod europe_pmc;
#[cfg(feature = "source-google_scholar")]
mod google_scholar;
#[cfg(feature = "source-hal")]
mod hal;
#[cfg(feature = "source-iacr")]
mod iacr;
#[cfg(feature = "source-ieee_xplore")]
mod ieee_xplore;
#[cfg(feature = "source-jstor")]
mod jstor;
#[cfg(feature = "source-mdpi")]
mod mdpi;
#[cfg(feature = "source-openalex")]
mod openalex;
#[cfg(feature = "source-osf")]
mod osf;
#[cfg(feature = "source-pmc")]
mod pmc;
#[cfg(feature = "source-pubmed")]
mod pubmed;
mod registry;
#[cfg(feature = "source-scispace")]
mod scispace;
#[cfg(feature = "source-semantic")]
mod semantic;
#[cfg(feature = "source-springer")]
mod springer;
#[cfg(feature = "source-ssrn")]
mod ssrn;
#[cfg(feature = "source-unpaywall")]
mod unpaywall;
#[cfg(feature = "source-worldwidescience")]
mod worldwidescience;
#[cfg(feature = "source-zenodo")]
mod zenodo;
pub mod mock;
pub use mock::MockSource;
pub use registry::{SourceCapabilities, SourceRegistry};
use crate::models::{
CitationRequest, DownloadRequest, DownloadResult, Paper, ReadRequest, ReadResult, SearchQuery,
SearchResponse,
};
use async_trait::async_trait;
#[async_trait]
pub trait Source: Send + Sync + std::fmt::Debug {
fn id(&self) -> &str;
fn name(&self) -> &str;
fn capabilities(&self) -> SourceCapabilities {
SourceCapabilities::SEARCH
}
fn supports_search(&self) -> bool {
self.capabilities().contains(SourceCapabilities::SEARCH)
}
fn supports_download(&self) -> bool {
self.capabilities().contains(SourceCapabilities::DOWNLOAD)
}
fn supports_read(&self) -> bool {
self.capabilities().contains(SourceCapabilities::READ)
}
fn supports_citations(&self) -> bool {
self.capabilities().contains(SourceCapabilities::CITATIONS)
}
fn supports_doi_lookup(&self) -> bool {
self.capabilities().contains(SourceCapabilities::DOI_LOOKUP)
}
fn supports_author_search(&self) -> bool {
self.capabilities()
.contains(SourceCapabilities::AUTHOR_SEARCH)
}
async fn search(&self, _query: &SearchQuery) -> Result<SearchResponse, SourceError> {
Err(SourceError::NotImplemented)
}
async fn search_by_author(
&self,
_author: &str,
_max_results: usize,
_year: Option<&str>,
) -> Result<SearchResponse, SourceError> {
Err(SourceError::NotImplemented)
}
async fn download(&self, _request: &DownloadRequest) -> Result<DownloadResult, SourceError> {
Err(SourceError::NotImplemented)
}
async fn read(&self, _request: &ReadRequest) -> Result<ReadResult, SourceError> {
Err(SourceError::NotImplemented)
}
async fn get_citations(
&self,
_request: &CitationRequest,
) -> Result<SearchResponse, SourceError> {
Err(SourceError::NotImplemented)
}
async fn get_references(
&self,
_request: &CitationRequest,
) -> Result<SearchResponse, SourceError> {
Err(SourceError::NotImplemented)
}
async fn get_related(&self, _request: &CitationRequest) -> Result<SearchResponse, SourceError> {
Err(SourceError::NotImplemented)
}
async fn get_by_doi(&self, _doi: &str) -> Result<Paper, SourceError> {
Err(SourceError::NotImplemented)
}
async fn get_by_id(&self, _id: &str) -> Result<Paper, SourceError> {
Err(SourceError::NotImplemented)
}
fn validate_id(&self, _id: &str) -> Result<(), SourceError> {
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum SourceError {
#[error("Operation not implemented for this source")]
NotImplemented,
#[error("Network error: {0}")]
Network(String),
#[error("Parse error: {0}")]
Parse(String),
#[error("Invalid request: {0}")]
InvalidRequest(String),
#[error("Rate limit exceeded")]
RateLimit,
#[error("Paper not found: {0}")]
NotFound(String),
#[error("API error: {0}")]
Api(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Error: {0}")]
Other(String),
}
impl From<reqwest::Error> for SourceError {
fn from(err: reqwest::Error) -> Self {
SourceError::Network(err.to_string())
}
}
impl From<serde_json::Error> for SourceError {
fn from(err: serde_json::Error) -> Self {
SourceError::Parse(format!("JSON: {}", err))
}
}
impl From<quick_xml::DeError> for SourceError {
fn from(err: quick_xml::DeError) -> Self {
SourceError::Parse(format!("XML: {}", err))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_capabilities() {
let caps = SourceCapabilities::SEARCH | SourceCapabilities::DOWNLOAD;
assert!(caps.contains(SourceCapabilities::SEARCH));
assert!(caps.contains(SourceCapabilities::DOWNLOAD));
assert!(!caps.contains(SourceCapabilities::CITATIONS));
}
}