use axum::http::header::CONTENT_TYPE;
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde::Serialize;
use serde_json::{json, Map, Value};
fn flatten_binding(node: &Value) -> Value {
let Some(obj) = node.as_object() else {
return node.clone();
};
match obj.get("type").and_then(Value::as_str) {
Some("uri") => obj.get("value").cloned().unwrap_or(Value::Null),
Some("literal") | Some("typed-literal") => {
let value = obj.get("value").and_then(Value::as_str).unwrap_or("");
match obj.get("datatype").and_then(Value::as_str) {
Some("http://www.w3.org/2001/XMLSchema#boolean") => Value::Bool(value == "true"),
Some("http://www.w3.org/2001/XMLSchema#integer") => value
.parse::<i64>()
.map(|n| json!(n))
.unwrap_or(Value::Null),
_ => Value::String(value.to_owned()),
}
}
_ => node.clone(),
}
}
pub fn results_to_array(results: &Value) -> Vec<Map<String, Value>> {
let vars: Vec<String> = results
.get("head")
.and_then(|h| h.get("vars"))
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default();
let empty = Vec::new();
let bindings = results
.get("results")
.and_then(|r| r.get("bindings"))
.and_then(Value::as_array)
.unwrap_or(&empty);
bindings
.iter()
.map(|binding| {
let mut row = Map::new();
for var in &vars {
let value = binding.get(var).map(flatten_binding).unwrap_or(Value::Null);
row.insert(var.clone(), value);
}
row
})
.collect()
}
fn json_array(rows: Vec<Value>) -> Response {
(
[(CONTENT_TYPE, "application/json")],
Json(Value::Array(rows)),
)
.into_response()
}
fn string_or_empty(row: &Map<String, Value>, key: &str) -> Value {
match row.get(key) {
Some(Value::String(s)) => Value::String(s.clone()),
_ => Value::String(String::new()),
}
}
fn string_or_null(row: &Map<String, Value>, key: &str) -> Value {
match row.get(key) {
Some(Value::String(s)) if !s.is_empty() => Value::String(s.clone()),
_ => Value::Null,
}
}
fn string_value(row: &Map<String, Value>, key: &str) -> String {
match row.get(key) {
Some(Value::String(s)) => s.clone(),
_ => String::new(),
}
}
fn string_option(row: &Map<String, Value>, key: &str) -> Option<String> {
match row.get(key) {
Some(Value::String(s)) if !s.is_empty() => Some(s.clone()),
_ => None,
}
}
pub fn metadata_response(results: &Value) -> Response {
let rows = results_to_array(results)
.into_iter()
.map(Value::Object)
.collect();
json_array(rows)
}
pub fn collections_response(results: &Value) -> Response {
let rows = results_to_array(results)
.into_iter()
.map(|row| {
json!({
"uri": row.get("Collection").cloned().unwrap_or(Value::Null),
"name": string_or_empty(&row, "name"),
"description": string_or_empty(&row, "description"),
"displayId": string_or_empty(&row, "displayId"),
"version": string_or_empty(&row, "version"),
})
})
.collect();
json_array(rows)
}
pub fn browse_response(results: &Value) -> Response {
let rows = results_to_array(results)
.into_iter()
.map(|row| {
let uri = row.get("Collection").and_then(Value::as_str).unwrap_or("");
json!({
"uri": uri,
"name": string_or_empty(&row, "name"),
"description": string_or_empty(&row, "description"),
"displayId": string_or_empty(&row, "displayId"),
"version": string_or_empty(&row, "version"),
"url": object_path(uri),
"public": uri.contains("/public/"),
})
})
.collect();
json_array(rows)
}
fn object_path(uri: &str) -> String {
for marker in ["/public/", "/user/"] {
if let Some(idx) = uri.find(marker) {
return uri[idx..].to_owned();
}
}
uri.to_owned()
}
#[derive(Serialize)]
struct SearchRow {
#[serde(rename = "type")]
type_iri: String,
uri: String,
name: String,
description: String,
#[serde(rename = "displayId")]
display_id: String,
version: String,
#[serde(rename = "sbolType")]
sbol_type: Option<String>,
role: Option<String>,
#[serde(rename = "percentMatch")]
percent_match: String,
#[serde(rename = "strandAlignment")]
strand_alignment: String,
#[serde(rename = "CIGAR")]
cigar: String,
}
pub fn search_response(results: &Value) -> Response {
let rows: Vec<SearchRow> = results_to_array(results)
.into_iter()
.map(|row| search_row(&row))
.collect();
(
[(CONTENT_TYPE, "application/json")],
serde_json::to_string(&rows).unwrap_or_else(|_| "[]".to_owned()),
)
.into_response()
}
fn search_row(row: &Map<String, Value>) -> SearchRow {
let display_id = string_value(row, "displayId");
let name = {
let name = string_value(row, "name");
if name.is_empty() {
display_id.clone()
} else {
name
}
};
SearchRow {
type_iri: string_value(row, "type"),
uri: string_value(row, "subject"),
name,
description: string_value(row, "description"),
display_id,
version: string_value(row, "version"),
sbol_type: string_option(row, "sbolType"),
role: string_option(row, "role"),
percent_match: string_value(row, "percentMatch"),
strand_alignment: string_value(row, "strandAlignment"),
cigar: string_value(row, "CIGAR"),
}
}
pub fn manage_response(results: &Value) -> Response {
let rows = results_to_array(results)
.into_iter()
.map(|row| manage_row(&row))
.collect();
json_array(rows)
}
fn manage_row(row: &Map<String, Value>) -> Value {
let uri = string_or_empty(row, "subject");
let uri_str = uri.as_str().unwrap_or("");
let type_iri = string_or_empty(row, "type");
let type_name = type_iri
.as_str()
.unwrap_or("")
.rsplit(['#', '/'])
.next()
.unwrap_or("")
.to_owned();
let url = uri_str
.split_once("://")
.and_then(|(_, authority_and_path)| {
authority_and_path
.find('/')
.map(|at| &authority_and_path[at..])
})
.unwrap_or(uri_str)
.to_owned();
let triplestore = if uri_str.contains("/public/") {
"public"
} else {
"private"
};
json!({
"uri": uri,
"name": string_or_empty(row, "name"),
"description": string_or_empty(row, "description"),
"displayId": string_or_empty(row, "displayId"),
"version": string_or_empty(row, "version"),
"type": type_iri,
"typeName": type_name,
"prefix": namespace_prefix(uri_str),
"url": url,
"role": string_or_null(row, "role"),
"sbolType": string_or_null(row, "sbolType"),
"triplestore": triplestore,
})
}
const NAMESPACE_PREFIXES: &[(&str, &str)] = &[
("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
("dcterms", "http://purl.org/dc/terms/"),
("dc", "http://purl.org/dc/elements/1.1/"),
("sbh", "http://wiki.synbiohub.org/wiki/Terms/synbiohub#"),
("prov", "http://www.w3.org/ns/prov#"),
("sbol", "http://sbols.org/v2#"),
("xsd", "http://www.w3.org/2001/XMLSchema#"),
("rdfs", "http://www.w3.org/2000/01/rdf-schema#"),
("purl", "http://purl.obolibrary.org/obo/"),
];
fn namespace_prefix(uri: &str) -> String {
NAMESPACE_PREFIXES
.iter()
.find(|(_, ns)| uri.starts_with(ns))
.map(|(name, _)| (*name).to_owned())
.unwrap_or_default()
}
pub fn count_response(results: &Value) -> Response {
(
[(CONTENT_TYPE, "text/plain")],
count_value(results).to_string(),
)
.into_response()
}
fn count_value(results: &Value) -> i64 {
results
.get("results")
.and_then(|r| r.get("bindings"))
.and_then(Value::as_array)
.and_then(|b| b.first())
.and_then(|row| row.get("count"))
.and_then(|c| c.get("value"))
.and_then(Value::as_str)
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
fn sparql(vars: &[&str], bindings: Value) -> Value {
json!({
"head": { "vars": vars },
"results": { "bindings": bindings },
})
}
#[test]
fn results_to_array_keys_every_var_and_flattens() {
let doc = sparql(
&["subject", "displayId", "version"],
json!([
{
"subject": { "type": "uri", "value": "http://ex/a" },
"displayId": { "type": "literal", "value": "a" },
},
]),
);
let rows = results_to_array(&doc);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["subject"], json!("http://ex/a"));
assert_eq!(rows[0]["displayId"], json!("a"));
assert_eq!(rows[0]["version"], Value::Null);
}
#[test]
fn integer_literals_are_coerced() {
let doc = sparql(
&["count"],
json!([
{
"count": {
"type": "literal",
"value": "7",
"datatype": "http://www.w3.org/2001/XMLSchema#integer",
},
},
]),
);
let rows = results_to_array(&doc);
assert_eq!(rows[0]["count"], json!(7));
assert_eq!(count_value(&doc), 7);
}
#[test]
fn count_value_defaults_to_zero() {
let doc = sparql(&["count"], json!([]));
assert_eq!(count_value(&doc), 0);
}
#[test]
fn namespace_prefix_matches_classic_prefixify() {
assert_eq!(
namespace_prefix("http://synbiohub.org/public/igem/BBa_B0012/1"),
""
);
assert_eq!(namespace_prefix("http://sbols.org/v2#Collection"), "sbol");
assert_eq!(
namespace_prefix("http://purl.org/dc/terms/description"),
"dcterms"
);
}
}