Skip to main content

acorn/io/api/
citeas.rs

1//! Module for communicating with CiteAs API
2//! > CiteAs is a way to get the correct citation for diverse research products including, software, datasets, preprints, and traditional articles. By making it easier to cite software and other "alternative" scholarly products, we aim to help the creators of such products get full credit for their work.
3//!
4//! See <https://citeas.org/api> for more information
5use crate::io::api::{ApiResult, Configuration, Param, Params, RemoteResource, INCLUDED_ENDPOINTS};
6use crate::param;
7use crate::prelude::var;
8use crate::schema::pid::DOI;
9use crate::util::{Label, Searchable};
10use async_trait::async_trait;
11use bon::Builder;
12use color_eyre::eyre::eyre;
13use serde::{Deserialize, Serialize};
14use serde_with::skip_serializing_none;
15use tracing::debug;
16
17/// CiteAs API options
18#[derive(Builder, Clone, Debug)]
19#[builder(start_fn = with_token, on(String, into))]
20pub struct Options {
21    /// Bearer token for authentication
22    #[builder(start_fn)]
23    pub token: String,
24    /// Request body payload
25    pub body: Option<String>,
26    /// CiteAs API domain (defaults to citeas.org)
27    #[builder(default = String::from("citeas.org"))]
28    pub domain: String,
29    /// Resource identifier
30    pub identifier: Option<String>,
31    /// Custom API parameters to include in every request
32    #[builder(default = vec![])]
33    pub custom_params: Vec<Param>,
34}
35impl Configuration for Options {
36    /// Build options from CiteAs environment variables
37    /// - `CITEAS_API_TOKEN` -> `token` (optional — public API)
38    /// - `CITEAS_SERVER_HOST` -> `domain` (defaults to citeas.org)
39    fn from_env() -> Self {
40        if let Err(why) = dotenvy::from_filename(".env") {
41            debug!("=> {} Load .env — {why}", Label::skip());
42        }
43        Self {
44            token: var("CITEAS_API_TOKEN").unwrap_or_default(),
45            body: None,
46            domain: var("CITEAS_SERVER_HOST").unwrap_or_else(|_| String::from("citeas.org")),
47            identifier: None,
48            custom_params: vec![],
49        }
50    }
51    fn with_body(self, value: impl Into<String>) -> Self {
52        Self {
53            body: Some(value.into()),
54            ..self
55        }
56    }
57    fn with_domain(self, value: impl Into<String>) -> Self {
58        Self {
59            domain: value.into(),
60            ..self
61        }
62    }
63    fn with_identifier(self, value: impl Into<String>) -> Self {
64        Self {
65            identifier: Some(value.into()),
66            ..self
67        }
68    }
69    fn token(&self) -> &str {
70        &self.token
71    }
72    fn domain(&self) -> &str {
73        &self.domain
74    }
75    fn identifier(&self) -> Option<&str> {
76        self.identifier.as_deref()
77    }
78    fn with_params(self, params: Vec<Param>) -> Self {
79        Self {
80            custom_params: params,
81            ..self
82        }
83    }
84    fn params(&self) -> &[Param] {
85        &self.custom_params
86    }
87}
88
89/// Trait for things that can be converted to citations (e.g., `DOI`)
90#[async_trait]
91pub trait ToCitations {
92    /// Convert to `Citations`
93    async fn to_citations(&self) -> ApiResult<Citations>;
94}
95/// Author object
96#[derive(Clone, Debug, Deserialize, Serialize)]
97pub struct Author {
98    /// First name
99    pub given: String,
100    /// Last name
101    pub family: String,
102}
103/// Main response object for the CiteAs API, returning citations for a given input
104#[derive(Clone, Debug, Deserialize, Serialize, Default)]
105pub struct Citations {
106    /// List of citation objects
107    pub citations: Vec<Citation>,
108    /// List of export objects
109    pub exports: Vec<Export>,
110    /// Metadata for listing all metadata found for a given resource.
111    /// <div class="warning">Varies by source</div>
112    pub metadata: Metadata,
113    /// Name of referenced resource
114    pub name: String,
115    /// List of provenance objects describing sources utilized to find and build citation data
116    pub provenance: Vec<Provenance>,
117    /// URL for the given resource
118    /// <div class="warning">If input is a keyword, the URL is the first Google search result for the given keyword</div>
119    pub url: String,
120}
121/// Citation API response object
122#[derive(Clone, Debug, Deserialize, Serialize)]
123pub struct Citation {
124    /// Citation entry
125    #[serde(alias = "citation")]
126    pub text: String,
127    /// Full name of the citation style
128    /// ### Example
129    /// > "American Psychological Association 6th edition"
130    pub style_fullname: String,
131    /// Short name of the citation style
132    /// ### Example
133    /// > "APA"
134    pub style_shortname: String,
135}
136/// Exported citation data
137#[derive(Clone, Debug, Deserialize, Serialize)]
138pub struct Export {
139    /// Citation export
140    pub export: String,
141    /// Export format
142    /// ### Note
143    /// > May include CSV, enw, [RIS], and [BibTeX].
144    ///
145    /// [RIS]: https://en.wikipedia.org/wiki/RIS_(file_format)
146    /// [BibTeX]: https://www.bibtex.org/
147    pub export_name: String,
148}
149/// Metadata for source
150/// <div class="warning">Varies by source</div>
151#[derive(Clone, Debug, Deserialize, Serialize, Default)]
152pub struct Metadata {
153    /// List of authors
154    pub author: Vec<Author>,
155    /// List of categories that resource applies to
156    pub categories: Vec<String>,
157    /// List of contributors
158    pub contributor: Vec<Author>,
159    /// Valid DOI
160    #[serde(alias = "DOI")]
161    pub doi: String,
162    /// ID for the resource
163    /// <div class="warning">Always "ITEM-1"</div>
164    pub id: String,
165    /// Publisher of resource
166    pub publisher: String,
167    /// Type of resource
168    #[serde(rename = "type")]
169    pub resource_type: String,
170    /// Title of the resource
171    /// ### Example
172    /// > "Oak Ridge National Laboratory (ORNL), Oak Ridge, TN (United States)"
173    pub title: String,
174    /// Resource URL
175    #[serde(alias = "URL")]
176    pub url: String,
177    /// Year of publication
178    pub year: u16,
179}
180/// Citation provenance object
181///
182/// Describes steps taken to try and find citation data, and whether citation data was found
183#[skip_serializing_none]
184#[derive(Clone, Debug, Deserialize, Serialize)]
185pub struct Provenance {
186    /// Additional URL utilized to discover citation data
187    pub additional_content_url: Option<String>,
188    /// URL utilized to discover citation data
189    pub content_url: Option<String>,
190    /// Original URL of the resource
191    pub original_url: Option<String>,
192    /// Returns "doi" or "arXiv ID" if found via DOI or arXiv, else "null"
193    pub found_via_proxy_type: Option<String>,
194    /// Returns true if content was found at the URL
195    pub has_content: bool,
196    /// Host of the resource, such as crossref, github or pypi
197    pub host: Option<String>,
198    /// Name of the step taken to find citation data
199    pub name: String,
200    /// Name of the parent step
201    pub parent_step_name: String,
202    /// Name of the parent subject
203    pub parent_subject: Option<String>,
204    /// Subject of the current step
205    /// ### Example
206    /// > "GitHub repository main page"
207    pub subject: String,
208    /// Resource keyword
209    pub key_word: Option<String>,
210}
211/// Describes status of the CiteAs API
212#[derive(Clone, Debug, Deserialize, Serialize)]
213pub struct StatusResponse {
214    /// Where you can find documentation for this version
215    /// ### Example
216    /// > "<https://citeas.org/api>"
217    pub documentation_url: String,
218    /// Relevant messages
219    /// ### Example
220    /// > "Don't panic"
221    pub msg: String,
222    /// API version
223    /// ### Example
224    /// > "0.1"
225    pub version: String,
226}
227/// Check if API is healthy
228/// ### Example
229/// ```ignore
230/// use acorn_lib::io::api;
231///
232/// println!("CiteAs API is healthy: {}", api::citeas::is_healthy().await);
233/// ```
234pub async fn is_healthy() -> bool {
235    match status().await {
236        | Ok(StatusResponse { msg, .. }) => msg.eq_ignore_ascii_case("Don't panic"),
237        | Err(_) => false,
238    }
239}
240/// Perform search on CiteAs API
241///
242/// The CiteAs API is simple and only has two endpoints. This accesses the endpoint for retrieving citation data for a [`DOI`]
243///
244/// ### Example
245/// ```ignore
246/// use acorn::param;
247/// use acorn::io::api::citeas;
248///
249/// let doi = "10.11578/dc.20250604.1";
250/// let options = citeas::Options::from_env()
251///     .with_params(vec![param!(TemplateValue, "doi", doi)]);
252/// let citations = citeas::search(&options).await;
253/// ```
254pub async fn search(options: &Options) -> ApiResult<Citations> {
255    let name = "CiteAs";
256    let action = "record";
257    let params = Params::new().with_custom(options.params()).build();
258    let data = Some(params);
259    match INCLUDED_ENDPOINTS.find_by_name(name) {
260        | Some(endpoint) => {
261            let response = endpoint.invoke(action, data).await;
262            endpoint.handle::<Citations>(response)
263        }
264        | None => Err(eyre!("{name} API endpoint not found")),
265    }
266}
267/// Get status of CiteAs API
268///
269/// See `https://citeas.org/api#api-status-object` for more information
270pub async fn status() -> ApiResult<StatusResponse> {
271    let name = "CiteAs";
272    let action = "status";
273    let data = None;
274    match INCLUDED_ENDPOINTS.find_by_name(name) {
275        | Some(endpoint) => {
276            let response = endpoint.invoke(action, data).await;
277            endpoint.handle::<StatusResponse>(response)
278        }
279        | None => Err(eyre!("{name} API endpoint not found")),
280    }
281}
282impl Citations {
283    /// Use CiteAs API to get citation data from a [`DOI`]
284    pub async fn from(value: DOI) -> ApiResult<Citations> {
285        let doi = value.to_string();
286        let options = Options::from_env().with_params(vec![param!(TemplateValue, "doi", &doi)]);
287        search(&options).await
288    }
289    /// Get citation data with given citation style (ex. "APA")
290    ///
291    /// If citation with desired style is not found, will return first citation
292    pub fn match_style(self, value: &str) -> Option<Citation> {
293        let citations = self.citations;
294        let result = citations
295            .iter()
296            .find(|&citation| citation.style_shortname.to_lowercase() == value.to_lowercase());
297        match result {
298            | Some(citation) => Some(citation.clone()),
299            | None => {
300                if citations.is_empty() {
301                    None
302                } else {
303                    match citations.first() {
304                        | Some(citation) => Some(citation.clone()),
305                        | None => None,
306                    }
307                }
308            }
309        }
310    }
311}
312#[async_trait]
313impl ToCitations for DOI {
314    /// Convert a [`DOI`] to a [`Citations`]
315    async fn to_citations(&self) -> ApiResult<Citations> {
316        Citations::from(self.clone()).await
317    }
318}