use crate::io::api::{ApiResult, Configuration, Param, Params, RemoteResource, INCLUDED_ENDPOINTS};
use crate::param;
use crate::prelude::var;
use crate::schema::pid::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
}
}
#[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 async fn from(value: DOI) -> ApiResult<Citations> {
let doi = value.to_string();
let options = Options::from_env().with_params(vec![param!(TemplateValue, "doi", &doi)]);
search(&options).await
}
pub fn match_style(self, value: &str) -> Option<Citation> {
let citations = self.citations;
let result = citations
.iter()
.find(|&citation| citation.style_shortname.to_lowercase() == value.to_lowercase());
match result {
| Some(citation) => Some(citation.clone()),
| None => {
if citations.is_empty() {
None
} else {
match citations.first() {
| Some(citation) => Some(citation.clone()),
| None => None,
}
}
}
}
}
}
#[async_trait]
impl ToCitations for DOI {
async fn to_citations(&self) -> ApiResult<Citations> {
Citations::from(self.clone()).await
}
}