use axum::extract::{Path, State};
use axum::response::Response;
use axum::Extension;
use sbol_db_app::{AlignOptions, SequenceAlignment, SimilarHit};
use sbol_db_core::{ObjectTerm, Triple, User};
use serde_json::{json, Map, Value};
use super::routes::{
public_pi_uri, public_uri, resolve_pi, scope_for, user_pi_uri, user_uri, PublicObject,
PublicObjectPi, UserObject, UserObjectPi,
};
use super::search::SequenceQuery;
use super::{render, CurrentUser};
use crate::{ApiError, AppState};
const VARS: [&str; 9] = [
"subject",
"displayId",
"version",
"name",
"description",
"type",
"percentMatch",
"strandAlignment",
"CIGAR",
];
const RDF_TYPE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
const DISPLAY_ID: &str = "http://sbols.org/v2#displayId";
const VERSION: &str = "http://sbols.org/v2#version";
const TITLE: &str = "http://purl.org/dc/terms/title";
const DESCRIPTION: &str = "http://purl.org/dc/terms/description";
pub(super) async fn run_sequence_search(
state: &AppState,
user: &Option<User>,
query: SequenceQuery,
) -> Result<Response, ApiError> {
let scope = scope_for(state, user).await?;
let options = AlignOptions {
mode: query.mode,
..AlignOptions::default()
};
let hits = state
.app
.sequence()
.align(&query.sequence, options, &scope)
.await?;
let mut bindings: Vec<Value> = Vec::with_capacity(hits.len());
for hit in &hits {
let meta = fetch_metadata(state, &hit.sequence_iri).await?;
bindings.push(alignment_binding(hit, &meta));
}
Ok(render::search_response(&solutions(bindings)))
}
pub(super) async fn run_sequence_count(
state: &AppState,
user: &Option<User>,
query: SequenceQuery,
) -> Result<Response, ApiError> {
let scope = scope_for(state, user).await?;
let options = AlignOptions {
mode: query.mode,
..AlignOptions::default()
};
let hits = state
.app
.sequence()
.align(&query.sequence, options, &scope)
.await?;
Ok(render::count_response(&count_solutions(hits.len())))
}
pub async fn public_similar(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(object): Path<PublicObject>,
) -> Result<Response, ApiError> {
similar_impl(state.clone(), user, public_uri(&state, &object)).await
}
pub async fn public_similar_count(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(object): Path<PublicObject>,
) -> Result<Response, ApiError> {
similar_count_impl(state.clone(), user, public_uri(&state, &object)).await
}
pub async fn user_similar(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(object): Path<UserObject>,
) -> Result<Response, ApiError> {
similar_impl(state.clone(), user, user_uri(&state, &object)).await
}
pub async fn user_similar_count(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(object): Path<UserObject>,
) -> Result<Response, ApiError> {
similar_count_impl(state.clone(), user, user_uri(&state, &object)).await
}
pub async fn public_similar_pi(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(object): Path<PublicObjectPi>,
) -> Result<Response, ApiError> {
let uri = resolve_pi(&state, &user, public_pi_uri(&state, &object)).await?;
similar_impl(state, user, uri).await
}
pub async fn public_similar_count_pi(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(object): Path<PublicObjectPi>,
) -> Result<Response, ApiError> {
let uri = resolve_pi(&state, &user, public_pi_uri(&state, &object)).await?;
similar_count_impl(state, user, uri).await
}
pub async fn user_similar_pi(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(object): Path<UserObjectPi>,
) -> Result<Response, ApiError> {
let uri = resolve_pi(&state, &user, user_pi_uri(&state, &object)).await?;
similar_impl(state, user, uri).await
}
pub async fn user_similar_count_pi(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(object): Path<UserObjectPi>,
) -> Result<Response, ApiError> {
let uri = resolve_pi(&state, &user, user_pi_uri(&state, &object)).await?;
similar_count_impl(state, user, uri).await
}
async fn similar_impl(
state: AppState,
user: Option<User>,
uri: String,
) -> Result<Response, ApiError> {
let scope = scope_for(&state, &user).await?;
let hits: Vec<SimilarHit> = state.app.sequence().similar(&uri, &scope).await?;
let mut bindings: Vec<Value> = Vec::with_capacity(hits.len());
for hit in &hits {
let meta = fetch_metadata(&state, &hit.iri).await?;
bindings.push(similar_binding(&hit.iri, &meta));
}
Ok(render::search_response(&solutions(bindings)))
}
async fn similar_count_impl(
state: AppState,
user: Option<User>,
uri: String,
) -> Result<Response, ApiError> {
let scope = scope_for(&state, &user).await?;
let count = state.app.sequence().similar_count(&uri, &scope).await?;
Ok(render::count_response(&count_solutions(count)))
}
#[derive(Default)]
struct RowMeta {
type_iri: Option<String>,
display_id: Option<String>,
version: Option<String>,
name: Option<String>,
description: Option<String>,
}
async fn fetch_metadata(state: &AppState, iri: &str) -> Result<RowMeta, ApiError> {
let triples = state.service.triples_for_subject(iri).await?;
Ok(project_metadata(&triples))
}
fn project_metadata(triples: &[Triple]) -> RowMeta {
let mut meta = RowMeta::default();
for triple in triples {
let predicate = triple.predicate.as_str();
match predicate {
RDF_TYPE => {
if let ObjectTerm::Iri(iri) = &triple.object {
let iri = iri.as_str();
let is_sbol = iri.contains("sbols.org");
let have_sbol = meta
.type_iri
.as_deref()
.is_some_and(|current| current.contains("sbols.org"));
if meta.type_iri.is_none() || (is_sbol && !have_sbol) {
meta.type_iri = Some(iri.to_owned());
}
}
}
DISPLAY_ID => set_first(&mut meta.display_id, literal_value(&triple.object)),
VERSION => set_first(&mut meta.version, literal_value(&triple.object)),
TITLE => set_first(&mut meta.name, literal_value(&triple.object)),
DESCRIPTION => set_first(&mut meta.description, literal_value(&triple.object)),
_ => {}
}
}
meta
}
fn set_first(slot: &mut Option<String>, value: Option<String>) {
if slot.is_none() {
*slot = value;
}
}
fn literal_value(object: &ObjectTerm) -> Option<String> {
match object {
ObjectTerm::Literal { value, .. } => Some(value.clone()),
_ => None,
}
}
fn alignment_binding(hit: &SequenceAlignment, meta: &RowMeta) -> Value {
let mut binding = base_binding(&hit.sequence_iri, meta);
binding.insert(
"percentMatch".to_owned(),
literal(&hit.percent_match.to_string()),
);
binding.insert(
"strandAlignment".to_owned(),
literal(&hit.strand.to_string()),
);
binding.insert("CIGAR".to_owned(), literal(&hit.cigar));
Value::Object(binding)
}
fn similar_binding(iri: &str, meta: &RowMeta) -> Value {
Value::Object(base_binding(iri, meta))
}
fn base_binding(iri: &str, meta: &RowMeta) -> Map<String, Value> {
let mut binding = Map::new();
binding.insert("subject".to_owned(), uri_node(iri));
if let Some(type_iri) = &meta.type_iri {
binding.insert("type".to_owned(), uri_node(type_iri));
}
if let Some(display_id) = &meta.display_id {
binding.insert("displayId".to_owned(), literal(display_id));
}
if let Some(version) = &meta.version {
binding.insert("version".to_owned(), literal(version));
}
if let Some(name) = &meta.name {
binding.insert("name".to_owned(), literal(name));
}
if let Some(description) = &meta.description {
binding.insert("description".to_owned(), literal(description));
}
binding
}
fn solutions(bindings: Vec<Value>) -> Value {
json!({
"head": { "vars": VARS },
"results": { "bindings": bindings },
})
}
fn count_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 literal(value: &str) -> Value {
json!({ "type": "literal", "value": value })
}