use crate::author::Author;
use crate::error::{OrcidError, Result};
use crate::search_builder::SearchBuilder;
use reqwest::header::ACCEPT;
use serde_json;
#[derive(Debug, Clone)]
pub struct Client {
base_url: String,
http_client: reqwest::Client,
}
impl Client {
pub fn new() -> Self {
Self {
base_url: "https://pub.orcid.org/v3.0/".to_string(),
http_client: reqwest::Client::new(),
}
}
pub fn http_client(mut self, client: reqwest::Client) -> Self {
self.http_client = client;
self
}
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
async fn get_json_from_api(&self, query: String) -> Result<serde_json::Value> {
let url = self.base_url.clone() + &query;
let response = self
.http_client
.get(&url)
.header(ACCEPT, "application/json")
.send()
.await?;
let json = response.json().await?;
Ok(json)
}
pub fn is_valid_orcid_id(id: &str) -> bool {
let mut digits: Vec<u32> = id
.chars()
.filter(|c| *c != '-')
.filter_map(|c| if c == 'X' { Some(10) } else { c.to_digit(10) })
.collect();
if digits.len() != 16 {
return false;
}
let last_digit = digits.pop().unwrap(); let total = digits.iter().fold(0, |total, digit| (total + digit) * 2);
let remainder = total % 11;
let result = (12 - remainder) % 11;
last_digit == result
}
pub async fn author(&self, orcid_id: &str) -> Result<Author> {
if !Self::is_valid_orcid_id(orcid_id) {
return Err(OrcidError::InvalidOrcidId(orcid_id.to_string()));
}
let json: serde_json::Value = self.get_json_from_api(orcid_id.to_string()).await?;
match json["error-code"].as_str() {
Some(error_code) => Err(OrcidError::ApiError {
orcid_id: orcid_id.to_string(),
error_code: error_code.to_string(),
developer_message: json["developer-message"]
.as_str()
.unwrap_or("no developer-message")
.to_string(),
}),
None => Ok(Author::new_from_json(json)),
}
}
pub async fn search_doi(&self, doi: &str) -> Result<Vec<String>> {
self.search(&("\"".to_string() + doi + "\"")).await
}
pub async fn search(&self, query: &str) -> Result<Vec<String>> {
let encoded_query = urlencoding::encode(query);
let json: serde_json::Value = self
.get_json_from_api(format!("search?q={}", encoded_query))
.await?;
match json["result"].as_array() {
Some(res) => Ok(res
.iter()
.filter_map(|x| x["orcid-identifier"]["path"].as_str())
.map(|s| s.to_string())
.collect()),
None => Err(OrcidError::BadApiResponse(json)),
}
}
pub fn search_builder(&self) -> SearchBuilder<'_> {
SearchBuilder::new(self)
}
}
impl Default for Client {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let client = Client::new();
assert_eq!(client.base_url, "https://pub.orcid.org/v3.0/");
}
#[test]
fn test_default() {
let client = Client::default();
assert_eq!(client.base_url, "https://pub.orcid.org/v3.0/");
}
#[test]
fn test_is_valid_orcid_id() {
assert!(Client::is_valid_orcid_id("0000-0001-5916-0947"));
assert!(Client::is_valid_orcid_id("0000000159160947"));
assert!(!Client::is_valid_orcid_id(
"0000-0001-6916-0947" ));
assert!(!Client::is_valid_orcid_id(
"0000-0001-5916-0948" ));
assert!(!Client::is_valid_orcid_id("12345"));
assert!(!Client::is_valid_orcid_id("xyz"));
}
#[test]
fn test_clone() {
let client = Client::new();
let cloned = client.clone();
assert_eq!(cloned.base_url, client.base_url);
}
#[test]
fn test_debug() {
let client = Client::new();
let debug_str = format!("{:?}", client);
assert!(debug_str.contains("Client"));
assert!(debug_str.contains("base_url"));
assert!(debug_str.contains("https://pub.orcid.org/v3.0/"));
}
#[test]
fn test_valid_orcid_with_x() {
assert!(Client::is_valid_orcid_id("0000-0002-1825-0097"));
assert!(Client::is_valid_orcid_id("0000000218250097"));
}
#[test]
fn test_invalid_orcid_length() {
assert!(!Client::is_valid_orcid_id("0000-0001-5916"));
assert!(!Client::is_valid_orcid_id("0000-0001-5916-0947-1234"));
assert!(!Client::is_valid_orcid_id(""));
}
#[test]
fn test_invalid_orcid_chars() {
assert!(!Client::is_valid_orcid_id("0000-0001-5916-094A"));
assert!(!Client::is_valid_orcid_id("0000-0001-5916-094?"));
assert!(!Client::is_valid_orcid_id("ABCD-EFGH-IJKL-MNOP"));
}
#[test]
fn test_base_url_override() {
let client = Client::new().base_url("http://localhost:1234/");
assert_eq!(client.base_url, "http://localhost:1234/");
}
#[test]
fn test_http_client_override() {
let custom = reqwest::Client::builder()
.user_agent("orcid-test/1.0")
.build()
.unwrap();
let client = Client::new().http_client(custom);
assert_eq!(client.base_url, "https://pub.orcid.org/v3.0/");
}
#[tokio::test]
async fn test_async_methods_return_futures() {
let client = Client::new();
let result = client.author("invalid").await;
assert!(result.is_err());
match result {
Err(OrcidError::InvalidOrcidId(_)) => (),
_ => panic!("Expected InvalidOrcidId error"),
}
}
#[tokio::test]
async fn test_search_with_empty_query() {
let client = Client::new();
let _result = client.search("").await;
}
#[tokio::test]
async fn test_search_doi_adds_quotes() {
let client = Client::new();
let _result = client.search_doi("10.1234/test").await;
}
}