use std::sync::Arc;
use std::time::{Duration, Instant};
use axum::extract::State;
use axum::Json;
use sbol_db_storage::RelationalSchema;
use serde::Serialize;
use tokio::sync::RwLock;
use crate::error::ApiError;
use crate::AppState;
const SCHEMA_TTL: Duration = Duration::from_secs(60);
const TOP_CLASSES_LIMIT: i64 = 25;
#[derive(Serialize, Clone)]
pub struct SparqlSchema {
pub prefixes: Vec<SparqlPrefix>,
pub top_classes: Vec<SparqlClass>,
}
#[derive(Serialize, Clone)]
pub struct SparqlPrefix {
pub prefix: String,
pub iri: String,
pub from_ontology: bool,
}
#[derive(Serialize, Clone)]
pub struct SparqlClass {
pub iri: String,
pub count: i64,
}
#[derive(Default)]
pub struct SchemaCache {
sql: RwLock<Option<CacheEntry<RelationalSchema>>>,
sparql: RwLock<Option<CacheEntry<SparqlSchema>>>,
overview: RwLock<Option<CacheEntry<super::overview::Overview>>>,
}
struct CacheEntry<T> {
at: Instant,
value: Arc<T>,
}
impl SchemaCache {
pub fn new() -> Self {
Self::default()
}
pub(super) async fn read_overview(&self) -> Option<Arc<super::overview::Overview>> {
read_fresh(&self.overview).await
}
pub(super) async fn write_overview(&self, value: Arc<super::overview::Overview>) {
*self.overview.write().await = Some(CacheEntry {
at: Instant::now(),
value,
});
}
pub async fn invalidate_all(&self) {
*self.sql.write().await = None;
*self.sparql.write().await = None;
*self.overview.write().await = None;
}
}
pub async fn sql(State(state): State<AppState>) -> Result<Json<Arc<RelationalSchema>>, ApiError> {
if let Some(hit) = read_fresh(&state.schema_cache.sql).await {
return Ok(Json(hit));
}
let schema = state.require_db_stats()?.schema_overview().await?;
let arc = Arc::new(schema);
*state.schema_cache.sql.write().await = Some(CacheEntry {
at: Instant::now(),
value: arc.clone(),
});
Ok(Json(arc))
}
pub async fn sparql(State(state): State<AppState>) -> Result<Json<Arc<SparqlSchema>>, ApiError> {
if let Some(hit) = read_fresh(&state.schema_cache.sparql).await {
return Ok(Json(hit));
}
let schema = load_sparql_schema(&state).await?;
let arc = Arc::new(schema);
*state.schema_cache.sparql.write().await = Some(CacheEntry {
at: Instant::now(),
value: arc.clone(),
});
Ok(Json(arc))
}
async fn read_fresh<T: Clone>(cell: &RwLock<Option<CacheEntry<T>>>) -> Option<Arc<T>> {
let guard = cell.read().await;
let entry = guard.as_ref()?;
if entry.at.elapsed() <= SCHEMA_TTL {
Some(Arc::clone(&entry.value))
} else {
None
}
}
async fn load_sparql_schema(state: &AppState) -> Result<SparqlSchema, ApiError> {
let mut prefixes: Vec<SparqlPrefix> = curated_prefixes()
.iter()
.map(|(p, i)| SparqlPrefix {
prefix: (*p).to_string(),
iri: (*i).to_string(),
from_ontology: false,
})
.collect();
let loaded = state.service.list_ontologies().await?;
for ont in loaded {
if prefixes
.iter()
.any(|p| p.prefix == ont.prefix.to_lowercase())
{
continue;
}
if let Some(iri) = ont.source_url.clone() {
prefixes.push(SparqlPrefix {
prefix: ont.prefix.to_lowercase(),
iri,
from_ontology: true,
});
}
}
let top_classes = state
.lab
.top_classes(TOP_CLASSES_LIMIT)
.await?
.into_iter()
.map(|c| SparqlClass {
iri: c.iri,
count: c.count,
})
.collect();
Ok(SparqlSchema {
prefixes,
top_classes,
})
}
fn curated_prefixes() -> &'static [(&'static str, &'static str)] {
&[
("sbol", "http://sbols.org/v3#"),
("prov", "http://www.w3.org/ns/prov#"),
(
"om",
"http://www.ontology-of-units-of-measure.org/resource/om-2/",
),
("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
("rdfs", "http://www.w3.org/2000/01/rdf-schema#"),
("owl", "http://www.w3.org/2002/07/owl#"),
("xsd", "http://www.w3.org/2001/XMLSchema#"),
]
}