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}