use crate::io::api::{ApiResult, Configuration, Param, Params, RemoteResource, TextResponse, INCLUDED_ENDPOINTS};
use crate::prelude::var;
use crate::schema::pid::{PersistentIdentifierParse, ROR};
use crate::util::{Label, Searchable};
use bon::Builder;
use color_eyre::eyre::eyre;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use tracing::debug;
use validator::Validate;
#[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("api.ror.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("ROR_API_TOKEN").unwrap_or_default(),
body: None,
domain: var("ROR_SERVER_HOST").unwrap_or_else(|_| String::from("api.ror.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
}
}
pub type StatusResponse = TextResponse;
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ExternalIdentifierType {
#[default]
Wikidata,
Isni,
Fundref,
Grid,
}
#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum LinkType {
Website,
Wikipedia,
Wikidata,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OrganizationNameType {
#[default]
RorDisplay,
Acronym,
Alias,
Label,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum OrganizationType {
#[default]
Facility,
Archive,
Company,
Education,
Funder,
Government,
Healthcare,
Nonprofit,
Other,
}
#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum RelationshipType {
Child,
Parent,
Related,
Successor,
Predecessor,
}
#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, Serialize)]
pub enum SchemaVersion {
#[serde(rename = "1.0")]
V1_0,
#[serde(rename = "2.0")]
V2_0,
#[serde(rename = "2.1")]
V2_1,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
#[default]
Active,
Inactive,
Withdrawn,
}
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
pub struct AdminField {
pub created: DateField,
pub last_modified: DateField,
}
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
pub struct DateField {
pub date: String,
pub schema_version: SchemaVersion,
}
#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
pub struct ExternalIdentifierField {
pub all: Vec<String>,
pub preferred: Option<String>,
#[serde(rename = "type")]
pub external_identifier_type: ExternalIdentifierType,
}
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
pub struct LinkField {
pub value: String,
#[serde(rename = "type")]
pub link_type: LinkType,
}
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)]
pub struct LocationDetailsField {
pub continent_code: String,
pub continent_name: String,
pub country_code: String,
pub country_name: String,
pub country_subdivision_code: String,
pub country_subdivision_name: String,
pub lat: f64,
pub lng: f64,
pub name: String,
}
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
pub struct LocationField {
#[serde(rename = "geonames_id")]
pub identifier: u64,
#[serde(rename = "geonames_details")]
pub details: LocationDetailsField,
}
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
pub struct MetadataItem {
#[serde(rename = "id")]
pub identifier: String,
pub title: String,
pub count: usize,
}
#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
pub struct NameField {
pub value: String,
pub lang: Option<String>,
pub types: Vec<OrganizationNameType>,
}
#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
pub struct RelationshipField {
pub label: String,
#[serde(rename = "type")]
pub relationship_type: RelationshipType,
#[serde(rename = "id")]
pub identifier: String,
}
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
pub struct SearchMetadata {
pub types: Vec<MetadataItem>,
pub countries: Vec<MetadataItem>,
pub continents: Vec<MetadataItem>,
pub statuses: Vec<MetadataItem>,
}
#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
pub struct SearchResponse {
pub number_of_results: usize,
pub time_taken: usize,
#[builder(default)]
pub items: Vec<SingleRecord>,
pub meta: SearchMetadata,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Deserialize, JsonSchema, Serialize, Validate)]
#[serde(rename_all = "snake_case")]
pub struct SingleRecord {
#[serde(rename = "id")]
pub identifier: String,
pub domains: Vec<String>,
pub established: Option<usize>,
pub external_ids: Vec<ExternalIdentifierField>,
pub links: Vec<LinkField>,
pub locations: Vec<LocationField>,
pub names: Vec<NameField>,
pub relationships: Vec<RelationshipField>,
pub status: Status,
pub types: Vec<OrganizationType>,
}
pub async fn is_healthy() -> bool {
match status().await {
| Ok(StatusResponse { content, .. }) => content.eq_ignore_ascii_case("OK"),
| Err(_) => false,
}
}
pub async fn record(options: &Options) -> ApiResult<SingleRecord> {
let value = options.identifier().unwrap_or_default().to_string();
if value.is_empty() || ROR::is_valid(&value) {
let name = "ROR";
let action = "search";
let params = Params::new()
.with_template("identifier", Some(&value))
.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::<SingleRecord>(response)
}
| None => Err(eyre!("{name} API endpoint not found")),
}
} else {
Err(eyre!("Invalid ROR identifier: {}", &value))
}
}
pub async fn search(options: &Options) -> ApiResult<SearchResponse> {
let name = "ROR";
let action = "search";
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::<SearchResponse>(response)
}
| None => Err(eyre!("{name} API endpoint not found")),
}
}
pub async fn status() -> ApiResult<StatusResponse> {
let name = "ROR";
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")),
}
}