use crate::analyzer::discovery::CitationFormat;
use crate::io::api::{ApiResult, Configuration, Param, Params, RemoteResource, INCLUDED_ENDPOINTS};
use crate::param;
use crate::prelude::var;
use crate::schema::pid::{PersistentIdentifier, PublicationIdentifierType, ARXIV, DOI};
use crate::util::{Label, Searchable};
use async_trait::async_trait;
use bon::Builder;
use color_eyre::eyre::eyre;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use tracing::debug;
#[derive(Builder, Clone, Debug)]
#[builder(start_fn = with_token, on(String, into))]
pub struct Options {
#[builder(start_fn)]
pub token: String,
pub body: Option<String>,
#[builder(default = String::from("citeas.org"))]
pub domain: String,
pub identifier: Option<String>,
#[builder(default = vec![])]
pub custom_params: Vec<Param>,
}
impl Configuration for Options {
fn from_env() -> Self {
if let Err(why) = dotenvy::from_filename(".env") {
debug!("=> {} Load .env — {why}", Label::skip());
}
Self {
token: var("CITEAS_API_TOKEN").unwrap_or_default(),
body: None,
domain: var("CITEAS_SERVER_HOST").unwrap_or_else(|_| String::from("citeas.org")),
identifier: None,
custom_params: vec![],
}
}
fn with_body(self, value: impl Into<String>) -> Self {
Self {
body: Some(value.into()),
..self
}
}
fn with_domain(self, value: impl Into<String>) -> Self {
Self {
domain: value.into(),
..self
}
}
fn with_identifier(self, value: impl Into<String>) -> Self {
Self {
identifier: Some(value.into()),
..self
}
}
fn token(&self) -> &str {
&self.token
}
fn domain(&self) -> &str {
&self.domain
}
fn identifier(&self) -> Option<&str> {
self.identifier.as_deref()
}
fn with_params(self, params: Vec<Param>) -> Self {
Self {
custom_params: params,
..self
}
}
fn params(&self) -> &[Param] {
&self.custom_params
}
}
impl From<DOI> for Options {
fn from(value: DOI) -> Self {
let value = value.to_string();
let identifier = urlencoding::encode(&value);
Self::from_env().with_params(vec![param!(TemplateValue, "identifier", identifier.as_ref())])
}
}
impl From<ARXIV> for Options {
fn from(value: ARXIV) -> Self {
let value = value.url();
let identifier = urlencoding::encode(&value);
Self::from_env().with_params(vec![param!(TemplateValue, "identifier", identifier.as_ref())])
}
}
impl From<PublicationIdentifierType> for Options {
fn from(value: PublicationIdentifierType) -> Self {
match value {
| PublicationIdentifierType::Doi(doi) => Self::from(doi),
| PublicationIdentifierType::Arxiv(arxiv) => Self::from(arxiv),
| PublicationIdentifierType::Unknown => Self::from_env(),
}
}
}
#[async_trait]
pub trait ToCitations {
async fn to_citations(&self) -> ApiResult<Citations>;
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Author {
pub given: String,
pub family: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
pub struct Citations {
pub citations: Vec<Citation>,
pub exports: Vec<Export>,
pub metadata: Metadata,
pub name: String,
pub provenance: Vec<Provenance>,
pub url: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Citation {
#[serde(alias = "citation")]
pub text: String,
pub style_fullname: String,
pub style_shortname: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Export {
pub export: String,
pub export_name: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
pub struct Metadata {
pub author: Vec<Author>,
pub categories: Vec<String>,
pub contributor: Vec<Author>,
#[serde(alias = "DOI")]
pub doi: String,
pub id: String,
pub publisher: String,
#[serde(rename = "type")]
pub resource_type: String,
pub title: String,
#[serde(alias = "URL")]
pub url: String,
pub year: u16,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Provenance {
pub additional_content_url: Option<String>,
pub content_url: Option<String>,
pub original_url: Option<String>,
pub found_via_proxy_type: Option<String>,
pub has_content: bool,
pub host: Option<String>,
pub name: String,
pub parent_step_name: String,
pub parent_subject: Option<String>,
pub subject: String,
pub key_word: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct StatusResponse {
pub documentation_url: String,
pub msg: String,
pub version: String,
}
pub async fn is_healthy() -> bool {
match status().await {
| Ok(StatusResponse { msg, .. }) => msg.eq_ignore_ascii_case("Don't panic"),
| Err(_) => false,
}
}
pub async fn search(options: &Options) -> ApiResult<Citations> {
let name = "CiteAs";
let action = "record";
let params = Params::new().with_custom(options.params()).build();
let data = Some(params);
match INCLUDED_ENDPOINTS.find_by_name(name) {
| Some(endpoint) => {
let response = endpoint.invoke(action, data).await;
endpoint.handle::<Citations>(response)
}
| None => Err(eyre!("{name} API endpoint not found")),
}
}
pub async fn status() -> ApiResult<StatusResponse> {
let name = "CiteAs";
let action = "status";
let data = None;
match INCLUDED_ENDPOINTS.find_by_name(name) {
| Some(endpoint) => {
let response = endpoint.invoke(action, data).await;
endpoint.handle::<StatusResponse>(response)
}
| None => Err(eyre!("{name} API endpoint not found")),
}
}
impl Citations {
pub fn match_style(self, value: CitationFormat) -> Option<Citation> {
let citations = self.citations;
let normalized_value = value.to_string();
let result = citations
.iter()
.find(|citation| citation.style_shortname.eq_ignore_ascii_case(&normalized_value));
result.cloned().or_else(|| citations.first().cloned())
}
}
#[async_trait]
impl ToCitations for DOI {
async fn to_citations(&self) -> ApiResult<Citations> {
search(&Options::from(self.clone())).await
}
}
#[async_trait]
impl ToCitations for ARXIV {
async fn to_citations(&self) -> ApiResult<Citations> {
search(&Options::from(self.clone())).await
}
}
#[async_trait]
impl ToCitations for PublicationIdentifierType {
async fn to_citations(&self) -> ApiResult<Citations> {
match self {
| Self::Doi(doi) => doi.to_citations().await,
| Self::Arxiv(arxiv) => arxiv.to_citations().await,
| Self::Unknown => Err(eyre!("Unsupported publication identifier")),
}
}
}