use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Extension;
use sbol_db_app::Hit;
use sbol_db_core::{DomainError, User};
use sbol_db_sparql::{GraphScope, ResultFormat, SparqlOptions};
use serde::Deserialize;
use serde_json::{json, Map, Value};
use super::search::{extract_sequence, has_sequence_facet, parse_search_path};
use super::{queries, render, sequence, CurrentUser};
use crate::{ApiError, AppState};
#[derive(Debug, Default, Deserialize)]
pub struct Paging {
pub offset: Option<usize>,
pub limit: Option<usize>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicObject {
pub collection_id: String,
pub display_id: String,
pub version: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserObject {
pub user_id: String,
pub collection_id: String,
pub display_id: String,
pub version: String,
}
pub(super) fn public_uri(state: &AppState, object: &PublicObject) -> String {
format!(
"{}public/{}/{}/{}",
state.app.registry_namespace.database_prefix(),
object.collection_id,
object.display_id,
object.version
)
}
pub(super) fn user_uri(state: &AppState, object: &UserObject) -> String {
format!(
"{}user/{}/{}/{}/{}",
state.app.registry_namespace.database_prefix(),
object.user_id,
object.collection_id,
object.display_id,
object.version
)
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicObjectPi {
pub collection_id: String,
pub display_id: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserObjectPi {
pub user_id: String,
pub collection_id: String,
pub display_id: String,
}
pub(super) fn public_pi_uri(state: &AppState, object: &PublicObjectPi) -> String {
format!(
"{}public/{}/{}",
state.app.registry_namespace.database_prefix(),
object.collection_id,
object.display_id
)
}
pub(super) fn user_pi_uri(state: &AppState, object: &UserObjectPi) -> String {
format!(
"{}user/{}/{}/{}",
state.app.registry_namespace.database_prefix(),
object.user_id,
object.collection_id,
object.display_id
)
}
#[derive(Debug, Default, Deserialize)]
pub struct SearchRootParams {
pub q: Option<String>,
pub object_type: Option<String>,
pub property_uri: Option<String>,
pub offset: Option<i64>,
pub limit: Option<i64>,
}
pub async fn search_root(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Query(params): Query<SearchRootParams>,
) -> Result<Response, ApiError> {
if let Some(q) = params.q {
let native = crate::routes::TextSearchParams {
q,
object_type: params.object_type,
property_uri: params.property_uri,
offset: params.offset.unwrap_or(0),
limit: params.limit.unwrap_or(crate::routes::SEARCH_DEFAULT_LIMIT),
};
return Ok(crate::routes::text_search(State(state), Query(native))
.await
.into_response());
}
let paging = Paging {
offset: params.offset.map(|o| o.max(0) as usize),
limit: params.limit.map(|l| l.max(0) as usize),
};
run_search(state, user, String::new(), paging).await
}
pub async fn search(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(query): Path<String>,
Query(paging): Query<Paging>,
) -> Result<Response, ApiError> {
run_search(state, user, query, paging).await
}
pub async fn search_count_root(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
) -> Result<Response, ApiError> {
run_search_count(state, user, String::new()).await
}
pub async fn search_count(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(query): Path<String>,
) -> Result<Response, ApiError> {
run_search_count(state, user, query).await
}
async fn run_search(
state: AppState,
user: Option<User>,
grammar: String,
paging: Paging,
) -> Result<Response, ApiError> {
if let Some(seq) = extract_sequence(&grammar) {
return sequence::run_sequence_search(&state, &user, seq).await;
}
if has_sequence_facet(&grammar) {
return Ok(render::search_response(&hits_to_solutions(&[])));
}
let mut faceted = parse_search_path(&grammar)?;
faceted.offset = paging.offset.unwrap_or(0);
faceted.limit = paging.limit;
let scope = scope_for(&state, &user).await?;
if faceted.free_text.is_some() {
let (hits, _total) = state.app.ranked_search(&faceted, scope).await?;
Ok(render::search_response(&hits_to_solutions(&hits)))
} else {
let results = run_scoped_value(&state, &queries::faceted(&faceted, false), scope).await?;
Ok(render::search_response(&results))
}
}
async fn run_search_count(
state: AppState,
user: Option<User>,
grammar: String,
) -> Result<Response, ApiError> {
if let Some(seq) = extract_sequence(&grammar) {
return sequence::run_sequence_count(&state, &user, seq).await;
}
if has_sequence_facet(&grammar) {
return Ok(render::count_response(&count_to_solutions(0)));
}
let faceted = parse_search_path(&grammar)?;
let scope = scope_for(&state, &user).await?;
if faceted.free_text.is_some() {
let count = state.app.ranked_search_count(&faceted, scope).await?;
Ok(render::count_response(&count_to_solutions(count)))
} else {
let results = run_scoped_value(&state, &queries::faceted(&faceted, true), scope).await?;
Ok(render::count_response(&results))
}
}
pub async fn type_count(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(type_name): Path<String>,
) -> Result<Response, ApiError> {
let scope = scope_for(&state, &user).await?;
let results = run_scoped_value(&state, &queries::count(&type_name), scope).await?;
Ok(render::count_response(&results))
}
pub async fn root_collections(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
) -> Result<Response, ApiError> {
let scope = scope_for(&state, &user).await?;
let results = run_scoped_value(&state, &queries::root_collections(), scope).await?;
Ok(render::collections_response(&results))
}
pub async fn browse(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
) -> Result<Response, ApiError> {
let scope = scope_for(&state, &user).await?;
let results = run_scoped_value(&state, &queries::root_collections(), scope).await?;
Ok(render::browse_response(&results))
}
pub async fn public_uses(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<PublicObject>,
) -> Result<Response, ApiError> {
uses_impl(
state.0.clone(),
user.0 .0,
public_uri(&state.0, &object),
false,
)
.await
}
pub async fn public_uses_count(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<PublicObject>,
) -> Result<Response, ApiError> {
uses_impl(
state.0.clone(),
user.0 .0,
public_uri(&state.0, &object),
true,
)
.await
}
pub async fn public_twins(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<PublicObject>,
) -> Result<Response, ApiError> {
twins_impl(
state.0.clone(),
user.0 .0,
public_uri(&state.0, &object),
false,
)
.await
}
pub async fn public_twins_count(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<PublicObject>,
) -> Result<Response, ApiError> {
twins_impl(
state.0.clone(),
user.0 .0,
public_uri(&state.0, &object),
true,
)
.await
}
pub async fn public_sub_collections(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<PublicObject>,
) -> Result<Response, ApiError> {
sub_collections_impl(state.0.clone(), user.0 .0, public_uri(&state.0, &object)).await
}
pub async fn public_metadata(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<PublicObject>,
) -> Result<Response, ApiError> {
metadata_impl(state.0.clone(), user.0 .0, public_uri(&state.0, &object)).await
}
pub async fn user_uses(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<UserObject>,
) -> Result<Response, ApiError> {
uses_impl(
state.0.clone(),
user.0 .0,
user_uri(&state.0, &object),
false,
)
.await
}
pub async fn user_uses_count(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<UserObject>,
) -> Result<Response, ApiError> {
uses_impl(
state.0.clone(),
user.0 .0,
user_uri(&state.0, &object),
true,
)
.await
}
pub async fn user_twins(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<UserObject>,
) -> Result<Response, ApiError> {
twins_impl(
state.0.clone(),
user.0 .0,
user_uri(&state.0, &object),
false,
)
.await
}
pub async fn user_twins_count(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<UserObject>,
) -> Result<Response, ApiError> {
twins_impl(
state.0.clone(),
user.0 .0,
user_uri(&state.0, &object),
true,
)
.await
}
pub async fn user_sub_collections(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<UserObject>,
) -> Result<Response, ApiError> {
sub_collections_impl(state.0.clone(), user.0 .0, user_uri(&state.0, &object)).await
}
pub async fn user_metadata(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<UserObject>,
) -> Result<Response, ApiError> {
metadata_impl(state.0.clone(), user.0 .0, user_uri(&state.0, &object)).await
}
pub(super) async fn resolve_pi(
state: &AppState,
user: &Option<User>,
pi: String,
) -> Result<String, ApiError> {
let scope = scope_for(state, user).await?;
super::download::latest_version_uri(state, scope, &pi)
.await?
.ok_or(ApiError::NotFound(pi))
}
pub async fn public_uses_pi(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<PublicObjectPi>,
) -> Result<Response, ApiError> {
let uri = resolve_pi(&state.0, &user.0 .0, public_pi_uri(&state.0, &object)).await?;
uses_impl(state.0.clone(), user.0 .0, uri, false).await
}
pub async fn public_uses_count_pi(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<PublicObjectPi>,
) -> Result<Response, ApiError> {
let uri = resolve_pi(&state.0, &user.0 .0, public_pi_uri(&state.0, &object)).await?;
uses_impl(state.0.clone(), user.0 .0, uri, true).await
}
pub async fn public_twins_pi(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<PublicObjectPi>,
) -> Result<Response, ApiError> {
let uri = resolve_pi(&state.0, &user.0 .0, public_pi_uri(&state.0, &object)).await?;
twins_impl(state.0.clone(), user.0 .0, uri, false).await
}
pub async fn public_twins_count_pi(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<PublicObjectPi>,
) -> Result<Response, ApiError> {
let uri = resolve_pi(&state.0, &user.0 .0, public_pi_uri(&state.0, &object)).await?;
twins_impl(state.0.clone(), user.0 .0, uri, true).await
}
pub async fn public_sub_collections_pi(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<PublicObjectPi>,
) -> Result<Response, ApiError> {
let uri = resolve_pi(&state.0, &user.0 .0, public_pi_uri(&state.0, &object)).await?;
sub_collections_impl(state.0.clone(), user.0 .0, uri).await
}
pub async fn public_metadata_pi(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<PublicObjectPi>,
) -> Result<Response, ApiError> {
let uri = resolve_pi(&state.0, &user.0 .0, public_pi_uri(&state.0, &object)).await?;
metadata_impl(state.0.clone(), user.0 .0, uri).await
}
pub async fn user_uses_pi(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<UserObjectPi>,
) -> Result<Response, ApiError> {
let uri = resolve_pi(&state.0, &user.0 .0, user_pi_uri(&state.0, &object)).await?;
uses_impl(state.0.clone(), user.0 .0, uri, false).await
}
pub async fn user_uses_count_pi(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<UserObjectPi>,
) -> Result<Response, ApiError> {
let uri = resolve_pi(&state.0, &user.0 .0, user_pi_uri(&state.0, &object)).await?;
uses_impl(state.0.clone(), user.0 .0, uri, true).await
}
pub async fn user_twins_pi(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<UserObjectPi>,
) -> Result<Response, ApiError> {
let uri = resolve_pi(&state.0, &user.0 .0, user_pi_uri(&state.0, &object)).await?;
twins_impl(state.0.clone(), user.0 .0, uri, false).await
}
pub async fn user_twins_count_pi(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<UserObjectPi>,
) -> Result<Response, ApiError> {
let uri = resolve_pi(&state.0, &user.0 .0, user_pi_uri(&state.0, &object)).await?;
twins_impl(state.0.clone(), user.0 .0, uri, true).await
}
pub async fn user_sub_collections_pi(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<UserObjectPi>,
) -> Result<Response, ApiError> {
let uri = resolve_pi(&state.0, &user.0 .0, user_pi_uri(&state.0, &object)).await?;
sub_collections_impl(state.0.clone(), user.0 .0, uri).await
}
pub async fn user_metadata_pi(
state: State<AppState>,
user: Extension<CurrentUser>,
Path(object): Path<UserObjectPi>,
) -> Result<Response, ApiError> {
let uri = resolve_pi(&state.0, &user.0 .0, user_pi_uri(&state.0, &object)).await?;
metadata_impl(state.0.clone(), user.0 .0, uri).await
}
async fn uses_impl(
state: AppState,
user: Option<User>,
uri: String,
count_only: bool,
) -> Result<Response, ApiError> {
let scope = scope_for(&state, &user).await?;
let results = run_scoped_value(&state, &queries::uses(&uri, count_only), scope).await?;
Ok(relation_response(&results, count_only))
}
async fn twins_impl(
state: AppState,
user: Option<User>,
uri: String,
count_only: bool,
) -> Result<Response, ApiError> {
let scope = scope_for(&state, &user).await?;
let results = run_scoped_value(&state, &queries::twins(&uri, count_only), scope).await?;
Ok(relation_response(&results, count_only))
}
fn relation_response(results: &Value, count_only: bool) -> Response {
if count_only {
render::count_response(results)
} else {
render::search_response(results)
}
}
async fn sub_collections_impl(
state: AppState,
user: Option<User>,
uri: String,
) -> Result<Response, ApiError> {
let scope = scope_for(&state, &user).await?;
let results = run_scoped_value(&state, &queries::sub_collections(&uri), scope).await?;
Ok(render::collections_response(&results))
}
async fn metadata_impl(
state: AppState,
user: Option<User>,
uri: String,
) -> Result<Response, ApiError> {
let scope = scope_for(&state, &user).await?;
let results = run_scoped_value(&state, &queries::metadata(&uri), scope).await?;
Ok(render::metadata_response(&results))
}
pub async fn manage(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
) -> Result<Response, ApiError> {
let Some(user) = user else {
return Ok(unauthorized());
};
let scope = state
.app
.acl_service
.compute_scope(Some(&user.graph_uri))
.await?;
let results = run_scoped_value(&state, &queries::owned_by(&user.graph_uri), scope).await?;
Ok(render::manage_response(&results))
}
pub async fn shared(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
) -> Result<Response, ApiError> {
let Some(user) = user else {
return Ok(unauthorized());
};
let scope = state
.app
.acl_service
.compute_scope(Some(&user.graph_uri))
.await?;
let objects = state.app.acl.viewable_objects(&user.graph_uri).await?;
let results = run_scoped_value(&state, &queries::metadata_of(&objects), scope).await?;
Ok(render::metadata_response(&results))
}
pub(super) async fn scope_for(
state: &AppState,
user: &Option<User>,
) -> Result<GraphScope, ApiError> {
let user_graph = user.as_ref().map(|u| u.graph_uri.clone());
let scope = state
.app
.acl_service
.compute_scope(user_graph.as_deref())
.await?;
Ok(scope)
}
pub(super) async fn run_scoped_value(
state: &AppState,
query: &str,
scope: GraphScope,
) -> Result<Value, ApiError> {
let default_graph = match &scope {
GraphScope::Only(graphs) if graphs.len() == 1 => Some(graphs[0].clone()),
_ => None,
};
let options = SparqlOptions {
authorized_graphs: scope,
..SparqlOptions::default()
};
let outcome = state
.app
.sparql
.execute(
query,
Some(ResultFormat::Json),
default_graph.as_deref(),
&options,
)
.await?;
serde_json::from_slice(&outcome.payload.body)
.map_err(|e| ApiError::Domain(DomainError::Serialization(e.to_string())))
}
fn unauthorized() -> Response {
(StatusCode::UNAUTHORIZED, "authentication required").into_response()
}
fn hits_to_solutions(hits: &[Hit]) -> Value {
let bindings: Vec<Value> = hits
.iter()
.map(|hit| {
let mut binding = Map::new();
binding.insert("subject".to_owned(), uri_node(&hit.subject));
insert_literal(&mut binding, "displayId", hit.display_id.as_deref());
insert_literal(&mut binding, "version", hit.version.as_deref());
insert_literal(&mut binding, "name", hit.name.as_deref());
insert_literal(&mut binding, "description", hit.description.as_deref());
if let Some(type_iri) = &hit.type_iri {
binding.insert("type".to_owned(), uri_node(type_iri));
}
Value::Object(binding)
})
.collect();
json!({
"head": { "vars": ["subject", "displayId", "version", "name", "description", "type"] },
"results": { "bindings": bindings },
})
}
fn count_to_solutions(count: usize) -> Value {
json!({
"head": { "vars": ["count"] },
"results": {
"bindings": [{
"count": {
"type": "literal",
"value": count.to_string(),
"datatype": "http://www.w3.org/2001/XMLSchema#integer",
},
}],
},
})
}
fn uri_node(value: &str) -> Value {
json!({ "type": "uri", "value": value })
}
fn insert_literal(binding: &mut Map<String, Value>, var: &str, value: Option<&str>) {
if let Some(value) = value {
binding.insert(var.to_owned(), json!({ "type": "literal", "value": value }));
}
}