use crate::io::api::{ApiResult, Configuration, Param, Params, RemoteResource, TextResponse, INCLUDED_ENDPOINTS};
use crate::util::constants::app::DEFAULT_ROR_DOMAIN;
use crate::util::constants::env::{ROR_API_TOKEN, ROR_SERVER_HOST};
use acorn_core::options::{ApiExtension, ApiOptions};
use acorn_core::util::Searchable;
use acorn_schema::pid::{PersistentIdentifierParse, ROR};
use acorn_schema::validation::Validate;
use bon::Builder;
use color_eyre::eyre::eyre;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
pub type Options = ApiOptions<Extension, Param>;
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(Clone, Debug, Default)]
pub struct Extension;
#[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>,
}
impl ApiExtension for Extension {
fn default_domain() -> String {
String::from(DEFAULT_ROR_DOMAIN)
}
fn env_token_var() -> &'static str {
ROR_API_TOKEN
}
fn env_domain_var() -> &'static str {
ROR_SERVER_HOST
}
}
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")),
}
}