//! Type-safe SciX query builder.
//!
//! Helps construct ADS query strings with proper escaping and syntax.
//!
//! # Example
//!
//! ```
//! use scix_client::QueryBuilder;
//!
//! let query = QueryBuilder::new()
//! .author("Einstein")
//! .and()
//! .year_range(1905, 1910)
//! .build();
//! assert_eq!(query, "author:\"Einstein\" AND year:[1905 TO 1910]");
//! ```
/// Builder for constructing ADS query strings.
#[derive(Debug, Clone, Default)]
pub struct QueryBuilder {
parts: Vec<String>,
}
impl QueryBuilder {
/// Create a new empty query builder.
pub fn new() -> Self {
Self::default()
}
/// Add an author search term.
pub fn author(mut self, name: &str) -> Self {
self.parts.push(format!("author:\"{}\"", name));
self
}
/// Add a first-author search term.
pub fn first_author(mut self, name: &str) -> Self {
self.parts.push(format!("first_author:\"{}\"", name));
self
}
/// Add a title search term.
pub fn title(mut self, text: &str) -> Self {
self.parts.push(format!("title:\"{}\"", text));
self
}
/// Add an abstract search term.
pub fn abstract_contains(mut self, text: &str) -> Self {
self.parts.push(format!("abs:\"{}\"", text));
self
}
/// Add an exact year filter.
pub fn year(mut self, year: u16) -> Self {
self.parts.push(format!("year:{}", year));
self
}
/// Add a year range filter (inclusive).
pub fn year_range(mut self, from: u16, to: u16) -> Self {
self.parts.push(format!("year:[{} TO {}]", from, to));
self
}
/// Add a bibcode filter.
pub fn bibcode(mut self, bibcode: &str) -> Self {
self.parts.push(format!("bibcode:{}", bibcode));
self
}
/// Add a DOI filter.
pub fn doi(mut self, doi: &str) -> Self {
self.parts.push(format!("doi:\"{}\"", doi));
self
}
/// Add an arXiv ID filter.
pub fn arxiv(mut self, arxiv_id: &str) -> Self {
self.parts.push(format!("identifier:arXiv:{}", arxiv_id));
self
}
/// Add an astronomical object filter.
pub fn object(mut self, name: &str) -> Self {
self.parts.push(format!("object:\"{}\"", name));
self
}
/// Add a bibstem (journal abbreviation) filter.
pub fn bibstem(mut self, stem: &str) -> Self {
self.parts.push(format!("bibstem:{}", stem));
self
}
/// Add a property filter (e.g., "refereed", "openaccess").
pub fn property(mut self, prop: &str) -> Self {
self.parts.push(format!("property:{}", prop));
self
}
/// Add a doctype filter (e.g., "article", "inproceedings").
pub fn doctype(mut self, dtype: &str) -> Self {
self.parts.push(format!("doctype:{}", dtype));
self
}
/// Add an ORCID filter.
pub fn orcid(mut self, orcid: &str) -> Self {
self.parts.push(format!("orcid:{}", orcid));
self
}
/// Add an AND operator.
pub fn and(mut self) -> Self {
self.parts.push("AND".to_string());
self
}
/// Add an OR operator.
pub fn or(mut self) -> Self {
self.parts.push("OR".to_string());
self
}
/// Add a NOT operator (exclude the following term).
pub fn exclude(mut self) -> Self {
self.parts.push("NOT".to_string());
self
}
/// Add a raw query fragment (no escaping).
pub fn raw(mut self, query: &str) -> Self {
self.parts.push(query.to_string());
self
}
/// Build a citations-of query.
pub fn citations_of(bibcode: &str) -> Self {
let mut qb = Self::new();
qb.parts.push(format!("citations(bibcode:{})", bibcode));
qb
}
/// Build a references-of query.
pub fn references_of(bibcode: &str) -> Self {
let mut qb = Self::new();
qb.parts.push(format!("references(bibcode:{})", bibcode));
qb
}
/// Build a similar-to query.
pub fn similar_to(bibcode: &str) -> Self {
let mut qb = Self::new();
qb.parts.push(format!("similar(bibcode:{})", bibcode));
qb
}
/// Build a trending query.
pub fn trending(bibcode: &str) -> Self {
let mut qb = Self::new();
qb.parts.push(format!("trending(bibcode:{})", bibcode));
qb
}
/// Build the final query string.
pub fn build(self) -> String {
self.parts.join(" ")
}
}
impl std::fmt::Display for QueryBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.parts.join(" "))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_author_query() {
let q = QueryBuilder::new().author("Einstein").build();
assert_eq!(q, "author:\"Einstein\"");
}
#[test]
fn test_combined_query() {
let q = QueryBuilder::new()
.author("Einstein")
.and()
.year_range(1905, 1910)
.build();
assert_eq!(q, "author:\"Einstein\" AND year:[1905 TO 1910]");
}
#[test]
fn test_complex_query() {
let q = QueryBuilder::new()
.first_author("Weinberg")
.and()
.title("cosmological constant")
.and()
.property("refereed")
.build();
assert_eq!(
q,
"first_author:\"Weinberg\" AND title:\"cosmological constant\" AND property:refereed"
);
}
#[test]
fn test_citations_of() {
let q = QueryBuilder::citations_of("2023ApJ...123..456A").build();
assert_eq!(q, "citations(bibcode:2023ApJ...123..456A)");
}
#[test]
fn test_raw_fragment() {
let q = QueryBuilder::new()
.raw("full_text:\"galaxy cluster\"")
.and()
.year(2023)
.build();
assert_eq!(q, "full_text:\"galaxy cluster\" AND year:2023");
}
#[test]
fn test_display_trait() {
let q = QueryBuilder::new().author("Hawking").and().year(1974);
assert_eq!(format!("{}", q), "author:\"Hawking\" AND year:1974");
}
}