1use crate::analyzer::discovery::CitationFormat;
6use crate::io::api::{ApiResult, Configuration, Param, Params, RemoteResource, INCLUDED_ENDPOINTS};
7use crate::param;
8use crate::prelude::var;
9use crate::schema::pid::{PersistentIdentifier, PublicationIdentifierType, ARXIV, DOI};
10use crate::util::{Label, Searchable};
11use async_trait::async_trait;
12use bon::Builder;
13use color_eyre::eyre::eyre;
14use serde::{Deserialize, Serialize};
15use serde_with::skip_serializing_none;
16use tracing::debug;
17
18#[derive(Builder, Clone, Debug)]
20#[builder(start_fn = with_token, on(String, into))]
21pub struct Options {
22 #[builder(start_fn)]
24 pub token: String,
25 pub body: Option<String>,
27 #[builder(default = String::from("citeas.org"))]
29 pub domain: String,
30 pub identifier: Option<String>,
32 #[builder(default = vec![])]
34 pub custom_params: Vec<Param>,
35}
36impl Configuration for Options {
37 fn from_env() -> Self {
41 if let Err(why) = dotenvy::from_filename(".env") {
42 debug!("=> {} Load .env — {why}", Label::skip());
43 }
44 Self {
45 token: var("CITEAS_API_TOKEN").unwrap_or_default(),
46 body: None,
47 domain: var("CITEAS_SERVER_HOST").unwrap_or_else(|_| String::from("citeas.org")),
48 identifier: None,
49 custom_params: vec![],
50 }
51 }
52 fn with_body(self, value: impl Into<String>) -> Self {
53 Self {
54 body: Some(value.into()),
55 ..self
56 }
57 }
58 fn with_domain(self, value: impl Into<String>) -> Self {
59 Self {
60 domain: value.into(),
61 ..self
62 }
63 }
64 fn with_identifier(self, value: impl Into<String>) -> Self {
65 Self {
66 identifier: Some(value.into()),
67 ..self
68 }
69 }
70 fn token(&self) -> &str {
71 &self.token
72 }
73 fn domain(&self) -> &str {
74 &self.domain
75 }
76 fn identifier(&self) -> Option<&str> {
77 self.identifier.as_deref()
78 }
79 fn with_params(self, params: Vec<Param>) -> Self {
80 Self {
81 custom_params: params,
82 ..self
83 }
84 }
85 fn params(&self) -> &[Param] {
86 &self.custom_params
87 }
88}
89impl From<DOI> for Options {
90 fn from(value: DOI) -> Self {
91 let value = value.to_string();
92 let identifier = urlencoding::encode(&value);
93 Self::from_env().with_params(vec![param!(TemplateValue, "identifier", identifier.as_ref())])
94 }
95}
96impl From<ARXIV> for Options {
97 fn from(value: ARXIV) -> Self {
98 let value = value.url();
99 let identifier = urlencoding::encode(&value);
100 Self::from_env().with_params(vec![param!(TemplateValue, "identifier", identifier.as_ref())])
101 }
102}
103impl From<PublicationIdentifierType> for Options {
104 fn from(value: PublicationIdentifierType) -> Self {
105 match value {
106 | PublicationIdentifierType::Doi(doi) => Self::from(doi),
107 | PublicationIdentifierType::Arxiv(arxiv) => Self::from(arxiv),
108 | PublicationIdentifierType::Unknown => Self::from_env(),
109 }
110 }
111}
112
113#[async_trait]
115pub trait ToCitations {
116 async fn to_citations(&self) -> ApiResult<Citations>;
118}
119#[derive(Clone, Debug, Deserialize, Serialize)]
121pub struct Author {
122 pub given: String,
124 pub family: String,
126}
127#[derive(Clone, Debug, Deserialize, Serialize, Default)]
129pub struct Citations {
130 pub citations: Vec<Citation>,
132 pub exports: Vec<Export>,
134 pub metadata: Metadata,
137 pub name: String,
139 pub provenance: Vec<Provenance>,
141 pub url: String,
144}
145#[derive(Clone, Debug, Deserialize, Serialize)]
147pub struct Citation {
148 #[serde(alias = "citation")]
150 pub text: String,
151 pub style_fullname: String,
155 pub style_shortname: String,
159}
160#[derive(Clone, Debug, Deserialize, Serialize)]
162pub struct Export {
163 pub export: String,
165 pub export_name: String,
172}
173#[derive(Clone, Debug, Deserialize, Serialize, Default)]
176pub struct Metadata {
177 pub author: Vec<Author>,
179 pub categories: Vec<String>,
181 pub contributor: Vec<Author>,
183 #[serde(alias = "DOI")]
185 pub doi: String,
186 pub id: String,
189 pub publisher: String,
191 #[serde(rename = "type")]
193 pub resource_type: String,
194 pub title: String,
198 #[serde(alias = "URL")]
200 pub url: String,
201 pub year: u16,
203}
204#[skip_serializing_none]
208#[derive(Clone, Debug, Deserialize, Serialize)]
209pub struct Provenance {
210 pub additional_content_url: Option<String>,
212 pub content_url: Option<String>,
214 pub original_url: Option<String>,
216 pub found_via_proxy_type: Option<String>,
218 pub has_content: bool,
220 pub host: Option<String>,
222 pub name: String,
224 pub parent_step_name: String,
226 pub parent_subject: Option<String>,
228 pub subject: String,
232 pub key_word: Option<String>,
234}
235#[derive(Clone, Debug, Deserialize, Serialize)]
237pub struct StatusResponse {
238 pub documentation_url: String,
242 pub msg: String,
246 pub version: String,
250}
251pub async fn is_healthy() -> bool {
259 match status().await {
260 | Ok(StatusResponse { msg, .. }) => msg.eq_ignore_ascii_case("Don't panic"),
261 | Err(_) => false,
262 }
263}
264pub async fn search(options: &Options) -> ApiResult<Citations> {
279 let name = "CiteAs";
280 let action = "record";
281 let params = Params::new().with_custom(options.params()).build();
282 let data = Some(params);
283 match INCLUDED_ENDPOINTS.find_by_name(name) {
284 | Some(endpoint) => {
285 let response = endpoint.invoke(action, data).await;
286 endpoint.handle::<Citations>(response)
287 }
288 | None => Err(eyre!("{name} API endpoint not found")),
289 }
290}
291pub async fn status() -> ApiResult<StatusResponse> {
295 let name = "CiteAs";
296 let action = "status";
297 let data = None;
298 match INCLUDED_ENDPOINTS.find_by_name(name) {
299 | Some(endpoint) => {
300 let response = endpoint.invoke(action, data).await;
301 endpoint.handle::<StatusResponse>(response)
302 }
303 | None => Err(eyre!("{name} API endpoint not found")),
304 }
305}
306impl Citations {
307 pub fn match_style(self, value: CitationFormat) -> Option<Citation> {
311 let citations = self.citations;
312 let normalized_value = value.to_string();
313 let result = citations
314 .iter()
315 .find(|citation| citation.style_shortname.eq_ignore_ascii_case(&normalized_value));
316 result.cloned().or_else(|| citations.first().cloned())
317 }
318}
319#[async_trait]
320impl ToCitations for DOI {
321 async fn to_citations(&self) -> ApiResult<Citations> {
323 search(&Options::from(self.clone())).await
324 }
325}
326#[async_trait]
327impl ToCitations for ARXIV {
328 async fn to_citations(&self) -> ApiResult<Citations> {
329 search(&Options::from(self.clone())).await
330 }
331}
332#[async_trait]
333impl ToCitations for PublicationIdentifierType {
334 async fn to_citations(&self) -> ApiResult<Citations> {
335 match self {
336 | Self::Doi(doi) => doi.to_citations().await,
337 | Self::Arxiv(arxiv) => arxiv.to_citations().await,
338 | Self::Unknown => Err(eyre!("Unsupported publication identifier")),
339 }
340 }
341}