//! Public types for the SciX client.
//!
//! These types are self-contained with no external dependencies beyond serde,
//! making this crate publishable to crates.io independently.
//!
//! When the `python` feature is enabled, all types are exposed to Python
//! via PyO3 with automatic field access (`get_all`).
use serde::{Deserialize, Serialize};
/// A paper (document) from ADS search results.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct Paper {
/// ADS bibcode (primary identifier).
pub bibcode: String,
/// Paper title.
pub title: String,
/// List of authors.
pub authors: Vec<Author>,
/// Publication year.
pub year: Option<u16>,
/// Journal/publication name.
pub publication: Option<String>,
/// Abstract text.
pub abstract_text: Option<String>,
/// DOI (first, if multiple).
pub doi: Option<String>,
/// arXiv ID (extracted from identifiers).
pub arxiv_id: Option<String>,
/// Raw identifier list from ADS.
pub identifiers: Vec<String>,
/// Electronic source flags (EPRINT_PDF, PUB_PDF, etc.).
pub esources: Vec<String>,
/// Number of citations.
pub citation_count: Option<u32>,
/// Document type (article, inproceedings, etc.).
pub doctype: Option<String>,
/// Property flags (OPENACCESS, REFEREED, etc.).
pub properties: Vec<String>,
/// Constructed PDF links, ordered by priority.
pub pdf_links: Vec<PdfLink>,
/// ADS URL for this paper.
pub url: String,
}
/// An author of a paper.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct Author {
/// Raw name as returned by ADS ("Last, First M.").
pub name: String,
/// Family (last) name.
pub family_name: String,
/// Given (first) name and initials.
pub given_name: Option<String>,
}
impl Author {
/// Parse an author name in ADS format ("Last, First M.").
pub fn from_ads_format(name: &str) -> Self {
let parts: Vec<&str> = name.splitn(2, ',').collect();
if parts.len() == 2 {
Author {
name: name.to_string(),
family_name: parts[0].trim().to_string(),
given_name: Some(parts[1].trim().to_string()),
}
} else {
let words: Vec<&str> = name.split_whitespace().collect();
if words.len() > 1 {
Author {
name: name.to_string(),
family_name: words.last().unwrap().to_string(),
given_name: Some(words[..words.len() - 1].join(" ")),
}
} else {
Author {
name: name.to_string(),
family_name: name.to_string(),
given_name: None,
}
}
}
}
/// Format as "First M. Last" for display.
pub fn display_name(&self) -> String {
match &self.given_name {
Some(given) => format!("{} {}", given, self.family_name),
None => self.family_name.clone(),
}
}
/// Format as "Last, First M." for BibTeX.
pub fn bibtex_name(&self) -> String {
match &self.given_name {
Some(given) => format!("{}, {}", self.family_name, given),
None => self.family_name.clone(),
}
}
}
/// A link to a PDF of a paper.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct PdfLink {
/// URL to the PDF or landing page.
pub url: String,
/// Type of link source.
pub link_type: PdfLinkType,
/// Human-readable label.
pub label: String,
}
/// Source type for a PDF link.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "python", pyo3::pyclass(eq))]
pub enum PdfLinkType {
ArXiv,
Publisher,
AdsScan,
Direct,
}
impl PdfLink {
/// Build PDF links from ADS esources, DOI, arXiv ID, and bibcode.
///
/// Priority: arXiv PDF > DOI/publisher > ADS scans > fallbacks.
pub fn from_esources(
esources: &[String],
doi: Option<&str>,
arxiv_id: Option<&str>,
bibcode: &str,
) -> Vec<PdfLink> {
let mut links = Vec::new();
let mut has_preprint = false;
let mut has_publisher = false;
for esource in esources {
let upper = esource.to_uppercase();
if upper == "EPRINT_PDF" {
if let Some(arxiv) = arxiv_id {
links.push(PdfLink {
url: format!("https://arxiv.org/pdf/{}.pdf", arxiv),
link_type: PdfLinkType::ArXiv,
label: "arXiv PDF".to_string(),
});
has_preprint = true;
}
} else if upper == "PUB_PDF" || upper == "PUB_HTML" {
if let Some(d) = doi {
if !d.is_empty() {
links.push(PdfLink {
url: format!("https://doi.org/{}", d),
link_type: PdfLinkType::Publisher,
label: "Publisher".to_string(),
});
has_publisher = true;
}
}
} else if upper == "ADS_PDF" || upper == "ADS_SCAN" {
links.push(PdfLink {
url: format!("https://articles.adsabs.harvard.edu/pdf/{}", bibcode),
link_type: PdfLinkType::AdsScan,
label: "ADS Scan".to_string(),
});
}
}
// Fallback links
if !has_preprint {
if let Some(arxiv) = arxiv_id {
links.push(PdfLink {
url: format!("https://arxiv.org/pdf/{}.pdf", arxiv),
link_type: PdfLinkType::ArXiv,
label: "arXiv PDF".to_string(),
});
}
}
if !has_publisher {
if let Some(d) = doi {
if !d.is_empty() {
links.push(PdfLink {
url: format!("https://doi.org/{}", d),
link_type: PdfLinkType::Publisher,
label: "Publisher".to_string(),
});
}
}
}
links
}
}
/// Paginated search response from ADS.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct SearchResponse {
/// Matching papers.
pub papers: Vec<Paper>,
/// Total number of results (may be larger than `papers.len()`).
pub num_found: u64,
}
/// Citation export formats supported by ADS.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyo3::pyclass(eq))]
pub enum ExportFormat {
BibTeX,
BibTeXAbs,
AasTex,
Icarus,
Mnras,
Soph,
Ris,
Endnote,
Medlars,
Ieee,
Csl,
DcXml,
RefXml,
RefAbsXml,
VoTable,
Rss,
Custom,
}
impl ExportFormat {
/// ADS API format string.
pub fn as_api_str(&self) -> &'static str {
match self {
Self::BibTeX => "bibtex",
Self::BibTeXAbs => "bibtexabs",
Self::AasTex => "aastex",
Self::Icarus => "icarus",
Self::Mnras => "mnras",
Self::Soph => "soph",
Self::Ris => "ris",
Self::Endnote => "endnote",
Self::Medlars => "medlars",
Self::Ieee => "ieee",
Self::Csl => "csl",
Self::DcXml => "dcxml",
Self::RefXml => "refxml",
Self::RefAbsXml => "refabsxml",
Self::VoTable => "votable",
Self::Rss => "rss",
Self::Custom => "custom",
}
}
/// Parse from string (case-insensitive).
pub fn from_str_loose(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"bibtex" => Some(Self::BibTeX),
"bibtexabs" => Some(Self::BibTeXAbs),
"aastex" => Some(Self::AasTex),
"icarus" => Some(Self::Icarus),
"mnras" => Some(Self::Mnras),
"soph" => Some(Self::Soph),
"ris" => Some(Self::Ris),
"endnote" => Some(Self::Endnote),
"medlars" => Some(Self::Medlars),
"ieee" => Some(Self::Ieee),
"csl" => Some(Self::Csl),
"dcxml" => Some(Self::DcXml),
"refxml" => Some(Self::RefXml),
"refabsxml" => Some(Self::RefAbsXml),
"votable" => Some(Self::VoTable),
"rss" => Some(Self::Rss),
"custom" => Some(Self::Custom),
_ => None,
}
}
}
impl std::fmt::Display for ExportFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_api_str())
}
}
/// Citation metrics for a set of papers.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct Metrics {
/// Basic statistics.
pub basic_stats: Option<BasicStats>,
/// Citation statistics.
pub citation_stats: Option<CitationStats>,
/// Bibliometric indicators.
pub indicators: Option<Indicators>,
}
/// Basic paper statistics.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct BasicStats {
#[serde(rename = "refereed")]
pub refereed: Option<BasicStatsEntry>,
#[serde(rename = "total")]
pub total: Option<BasicStatsEntry>,
}
/// Basic statistics entry (for refereed or total).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct BasicStatsEntry {
pub number_of_papers: Option<u32>,
pub normalized_paper_count: Option<f64>,
pub total_citations: Option<u32>,
pub total_normalized_citations: Option<f64>,
pub median_refereed_citations: Option<f64>,
pub mean_refereed_citations: Option<f64>,
}
/// Citation statistics.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct CitationStats {
#[serde(rename = "refereed")]
pub refereed: Option<CitationStatsEntry>,
#[serde(rename = "total")]
pub total: Option<CitationStatsEntry>,
}
/// Citation statistics entry.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct CitationStatsEntry {
pub number_of_citing_papers: Option<u32>,
pub total_citations: Option<u32>,
pub number_of_self_citations: Option<u32>,
pub average_citations: Option<f64>,
pub normalized_citations: Option<f64>,
}
/// Bibliometric indicators (h-index, g-index, etc.).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct Indicators {
pub h: Option<u32>,
pub g: Option<u32>,
pub i10: Option<u32>,
pub i100: Option<u32>,
pub m: Option<f64>,
pub tori: Option<f64>,
pub riq: Option<u32>,
pub read10: Option<f64>,
}
/// An ADS personal library.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct Library {
pub id: String,
pub name: String,
pub description: String,
pub num_documents: u32,
pub public: bool,
pub owner: String,
pub date_created: String,
pub date_last_modified: String,
}
/// Detailed library including documents.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct LibraryDetail {
pub metadata: Library,
pub documents: Vec<String>,
}
/// Result of astronomical object resolution.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct ObjectResult {
pub object: String,
pub bibcodes: Vec<String>,
}
/// Result of free-text reference resolution.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct ResolvedReference {
pub reference: String,
pub bibcode: Option<String>,
pub score: Option<String>,
}
/// Sort specification for search queries.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct Sort {
pub field: String,
pub direction: SortDirection,
}
/// Sort direction.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "python", pyo3::pyclass(eq))]
pub enum SortDirection {
Asc,
Desc,
}
impl Sort {
pub fn new(field: impl Into<String>, direction: SortDirection) -> Self {
Self {
field: field.into(),
direction,
}
}
pub fn date_desc() -> Self {
Self::new("date", SortDirection::Desc)
}
pub fn citation_count_desc() -> Self {
Self::new("citation_count", SortDirection::Desc)
}
pub fn score_desc() -> Self {
Self::new("score", SortDirection::Desc)
}
}
impl std::fmt::Display for Sort {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let dir = match self.direction {
SortDirection::Asc => "asc",
SortDirection::Desc => "desc",
};
write!(f, "{} {}", self.field, dir)
}
}