use axum::body::Bytes;
use axum::extract::{Path, Query, State};
use axum::http::header::{CONTENT_TYPE, LOCATION};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::{Extension, Json};
use sbol_db_app::{EditService, FieldValue, MakePublicRequest, MutationService};
use serde::Deserialize;
use serde_json::json;
use super::auth::{require_user, scope_for, Identity};
use super::download::{render_download, DownloadFormat};
use super::negotiate::{content_type_for, negotiate, Negotiated};
use super::search::{run_search, SearchParams, SearchResponse};
use super::util::{encode_iri_segment, parse_json, resolve_overwrite};
use crate::error::ApiError;
use crate::export;
use crate::v2::error::V2Error;
use crate::AppState;
const DCTERMS_TITLE: &str = "http://purl.org/dc/terms/title";
const DCTERMS_DESCRIPTION: &str = "http://purl.org/dc/terms/description";
pub async fn list_objects(
State(state): State<AppState>,
Extension(identity): Extension<Identity>,
Query(params): Query<SearchParams>,
) -> Result<Json<SearchResponse>, V2Error> {
Ok(Json(run_search(&state, &identity, params).await?))
}
#[derive(Debug, Default, Deserialize)]
pub struct GetObjectParams {
pub format: Option<String>,
pub version: Option<String>,
}
pub async fn get_object(
State(state): State<AppState>,
Extension(identity): Extension<Identity>,
Path(iri): Path<String>,
Query(params): Query<GetObjectParams>,
headers: axum::http::HeaderMap,
) -> Result<Response, V2Error> {
let requested_scope = scope_for(&state, &identity).await?;
let scope = state
.app
.object_read_scope(&iri, requested_scope)
.await?
.ok_or_else(|| not_found(&iri))?;
let sbol2 = wants_sbol2(params.version.as_deref())?;
if let Some(format) = params.format.as_deref() {
let format = DownloadFormat::parse(format).ok_or_else(|| {
V2Error::from(ApiError::BadRequest(format!(
"unsupported format: {format}"
)))
})?;
let display_id = state
.service
.get_object_by_iri(&iri)
.await
.map_err(V2Error::from)?
.and_then(|r| r.display_id)
.unwrap_or_else(|| last_segment(&iri));
return render_download(&state, &iri, &display_id, format, sbol2, scope).await;
}
match negotiate(&headers)? {
Negotiated::Json => {
let record = state
.service
.get_object_by_iri(&iri)
.await
.map_err(V2Error::from)?
.ok_or_else(|| not_found(&iri))?;
Ok(Json(record).into_response())
}
Negotiated::Rdf(format) => {
let body = export::export_subject_rdf(state.service.as_ref(), &iri, format, sbol2)
.await
.map_err(V2Error::from)?;
Ok((
[(CONTENT_TYPE, content_type_for(Negotiated::Rdf(format)))],
body,
)
.into_response())
}
}
}
pub async fn get_object_details(
State(state): State<AppState>,
Extension(identity): Extension<Identity>,
Path(iri): Path<String>,
) -> Result<Response, V2Error> {
let scope = scope_for(&state, &identity).await?;
let details = state
.app
.object_details(&iri, scope)
.await?
.ok_or_else(|| not_found(&iri))?;
Ok(Json(details).into_response())
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct PatchBody {
pub name: Option<String>,
pub description: Option<String>,
pub mutable_description: Option<String>,
pub mutable_notes: Option<String>,
pub mutable_source: Option<String>,
pub citations: Option<Vec<String>>,
}
pub async fn patch_object(
State(state): State<AppState>,
Extension(identity): Extension<Identity>,
Path(iri): Path<String>,
body: Bytes,
) -> Result<Response, V2Error> {
let user = require_user(&identity)?;
let patch: PatchBody = parse_json(&body)?;
let svc = edit_service(&state);
if let Some(name) = &patch.name {
svc.edit_field(
&user.graph_uri,
user.is_admin,
&iri,
DCTERMS_TITLE,
&FieldValue::Literal(name.clone()),
None,
)
.await?;
}
if let Some(description) = &patch.description {
svc.edit_field(
&user.graph_uri,
user.is_admin,
&iri,
DCTERMS_DESCRIPTION,
&FieldValue::Literal(description.clone()),
None,
)
.await?;
}
if let Some(value) = &patch.mutable_description {
svc.update_mutable_description(&user.graph_uri, user.is_admin, &iri, value)
.await?;
}
if let Some(value) = &patch.mutable_notes {
svc.update_mutable_notes(&user.graph_uri, user.is_admin, &iri, value)
.await?;
}
if let Some(value) = &patch.mutable_source {
svc.update_mutable_source(&user.graph_uri, user.is_admin, &iri, value)
.await?;
}
if let Some(citations) = &patch.citations {
svc.update_citations(&user.graph_uri, user.is_admin, &iri, citations)
.await?;
}
match state
.service
.get_object_by_iri(&iri)
.await
.map_err(V2Error::from)?
{
Some(record) => Ok(Json(record).into_response()),
None => Ok(Json(json!({ "iri": iri })).into_response()),
}
}
pub async fn delete_object(
State(state): State<AppState>,
Extension(identity): Extension<Identity>,
Path(iri): Path<String>,
) -> Result<StatusCode, V2Error> {
let user = require_user(&identity)?;
mutation_service(&state)
.remove(&user.graph_uri, user.is_admin, &iri)
.await?;
Ok(StatusCode::NO_CONTENT)
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct PublishBody {
pub id: Option<String>,
pub version: Option<String>,
pub name: Option<String>,
pub description: Option<String>,
pub citations: Vec<String>,
pub overwrite: Option<String>,
}
pub async fn publish_object(
State(state): State<AppState>,
Extension(identity): Extension<Identity>,
Path(iri): Path<String>,
body: Bytes,
) -> Result<Response, V2Error> {
let user = require_user(&identity)?;
let form: PublishBody = parse_json(&body)?;
let public_id = super::util::required(form.id, "id")?;
let version = super::util::required(form.version, "version")?;
let request = MakePublicRequest {
source_uri: iri.clone(),
owner_username: user.username.clone(),
public_id,
version,
name: form.name.filter(|s| !s.is_empty()),
description: form.description.filter(|s| !s.is_empty()),
creator_name: Some(user.name.clone()),
citations: form.citations,
overwrite: resolve_overwrite(form.overwrite.as_deref())?,
};
let outcome = mutation_service(&state)
.make_public(&user.graph_uri, user.is_admin, request)
.await?;
let members: Vec<&str> = outcome.members.iter().map(|m| m.as_str()).collect();
let payload = json!({
"collection_uri": outcome.collection_uri.as_str(),
"members": members,
"triple_count": outcome.triple_count,
});
let location = format!(
"/api/v2/objects/{}",
encode_iri_segment(outcome.collection_uri.as_str())
);
Ok((StatusCode::CREATED, [(LOCATION, location)], Json(payload)).into_response())
}
fn edit_service(state: &AppState) -> EditService {
state.app.edit_service()
}
fn mutation_service(state: &AppState) -> MutationService {
state.app.mutation_service()
}
fn not_found(iri: &str) -> V2Error {
V2Error::from(ApiError::NotFound(format!("object {iri}")))
}
fn wants_sbol2(version: Option<&str>) -> Result<bool, V2Error> {
match version.unwrap_or("sbol3") {
"sbol3" | "3" => Ok(false),
"sbol2" | "2" => Ok(true),
other => Err(V2Error::from(ApiError::BadRequest(format!(
"unknown version: {other}"
)))),
}
}
fn last_segment(iri: &str) -> String {
iri.rsplit(['/', '#']).next().unwrap_or(iri).to_owned()
}