use axum::body::Bytes;
use axum::extract::{Path, State};
use axum::http::HeaderMap;
use axum::response::{IntoResponse, Response};
use axum::Extension;
use sbol_db_app::{EditService, FieldValue};
use serde::Deserialize;
use super::CurrentUser;
use crate::{ApiError, AppState};
const DCTERMS_TITLE: &str = "http://purl.org/dc/terms/title";
const DCTERMS_DESCRIPTION: &str = "http://purl.org/dc/terms/description";
const SBOL2_ROLE: &str = "http://sbols.org/v2#role";
const SBOL2_TYPE: &str = "http://sbols.org/v2#type";
const PROV_WAS_DERIVED_FROM: &str = "http://www.w3.org/ns/prov#wasDerivedFrom";
fn service(state: &AppState) -> EditService {
state.app.edit_service()
}
fn require_user(user: Option<sbol_db_core::User>) -> Result<sbol_db_core::User, ApiError> {
user.ok_or_else(|| ApiError::Forbidden("authentication is required to edit".to_owned()))
}
fn success() -> Response {
(axum::http::StatusCode::OK, "Success").into_response()
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct MutableForm {
pub uri: Option<String>,
pub value: Option<String>,
}
pub async fn update_mutable_description(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
let user = require_user(user)?;
let form = parse_body::<MutableForm>(&headers, &body)?;
let uri = required(form.uri, "uri")?;
service(&state)
.update_mutable_description(&user.graph_uri, user.is_admin, &uri, &value_of(&form.value))
.await?;
Ok(success())
}
pub async fn update_mutable_notes(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
let user = require_user(user)?;
let form = parse_body::<MutableForm>(&headers, &body)?;
let uri = required(form.uri, "uri")?;
service(&state)
.update_mutable_notes(&user.graph_uri, user.is_admin, &uri, &value_of(&form.value))
.await?;
Ok(success())
}
pub async fn update_mutable_source(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
let user = require_user(user)?;
let form = parse_body::<MutableForm>(&headers, &body)?;
let uri = required(form.uri, "uri")?;
service(&state)
.update_mutable_source(&user.graph_uri, user.is_admin, &uri, &value_of(&form.value))
.await?;
Ok(success())
}
pub async fn update_citations(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
let user = require_user(user)?;
let form = parse_body::<MutableForm>(&headers, &body)?;
let uri = required(form.uri, "uri")?;
let citations = parse_citations(form.value.as_deref())?;
service(&state)
.update_citations(&user.graph_uri, user.is_admin, &uri, &citations)
.await?;
Ok(success())
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct FieldForm {
pub object: Option<String>,
pub previous: Option<String>,
pub pred: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserFieldPath {
pub user_id: String,
pub collection_id: String,
pub display_id: String,
pub version: String,
pub field: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicFieldPath {
pub collection_id: String,
pub display_id: String,
pub version: String,
pub field: String,
}
fn user_field_uri(state: &AppState, p: &UserFieldPath) -> String {
format!(
"{}user/{}/{}/{}/{}",
state.app.registry_namespace.database_prefix(),
p.user_id,
p.collection_id,
p.display_id,
p.version
)
}
fn public_field_uri(state: &AppState, p: &PublicFieldPath) -> String {
format!(
"{}public/{}/{}/{}",
state.app.registry_namespace.database_prefix(),
p.collection_id,
p.display_id,
p.version
)
}
enum Verb {
Edit,
Add,
Remove,
}
pub async fn user_edit_field(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(p): Path<UserFieldPath>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
run_field(
state.clone(),
user,
user_field_uri(&state, &p),
&p.field,
Verb::Edit,
&headers,
&body,
)
.await
}
pub async fn user_add_field(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(p): Path<UserFieldPath>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
run_field(
state.clone(),
user,
user_field_uri(&state, &p),
&p.field,
Verb::Add,
&headers,
&body,
)
.await
}
pub async fn user_remove_field(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(p): Path<UserFieldPath>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
run_field(
state.clone(),
user,
user_field_uri(&state, &p),
&p.field,
Verb::Remove,
&headers,
&body,
)
.await
}
pub async fn public_edit_field(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(p): Path<PublicFieldPath>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
run_field(
state.clone(),
user,
public_field_uri(&state, &p),
&p.field,
Verb::Edit,
&headers,
&body,
)
.await
}
pub async fn public_add_field(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(p): Path<PublicFieldPath>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
run_field(
state.clone(),
user,
public_field_uri(&state, &p),
&p.field,
Verb::Add,
&headers,
&body,
)
.await
}
pub async fn public_remove_field(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(p): Path<PublicFieldPath>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
run_field(
state.clone(),
user,
public_field_uri(&state, &p),
&p.field,
Verb::Remove,
&headers,
&body,
)
.await
}
async fn run_field(
state: AppState,
user: Option<sbol_db_core::User>,
uri: String,
field: &str,
verb: Verb,
headers: &HeaderMap,
body: &[u8],
) -> Result<Response, ApiError> {
let user = require_user(user)?;
let form = parse_body::<FieldForm>(headers, body)?;
let predicate = resolve_predicate(field, form.pred.as_deref())?;
let object = form
.object
.filter(|s| !s.is_empty())
.ok_or_else(|| ApiError::BadRequest("object is required".to_owned()))?;
let value = format_field_value(field, &object);
let svc = service(&state);
match verb {
Verb::Edit => {
let previous = if field == "title" || field == "description" {
None
} else {
form.previous
.filter(|s| !s.is_empty())
.map(|p| format_field_value(field, &p))
};
svc.edit_field(
&user.graph_uri,
user.is_admin,
&uri,
&predicate,
&value,
previous.as_ref(),
)
.await?;
}
Verb::Add => {
svc.add_field(&user.graph_uri, user.is_admin, &uri, &predicate, &value)
.await?;
}
Verb::Remove => {
svc.remove_field(&user.graph_uri, user.is_admin, &uri, &predicate, &value)
.await?;
}
}
Ok(success())
}
fn resolve_predicate(field: &str, pred: Option<&str>) -> Result<String, ApiError> {
match field {
"title" => Ok(DCTERMS_TITLE.to_owned()),
"description" => Ok(DCTERMS_DESCRIPTION.to_owned()),
"role" => Ok(SBOL2_ROLE.to_owned()),
"type" => Ok(SBOL2_TYPE.to_owned()),
"wasDerivedFrom" => Ok(PROV_WAS_DERIVED_FROM.to_owned()),
"annotation" => pred
.filter(|s| !s.is_empty())
.map(str::to_owned)
.ok_or_else(|| ApiError::BadRequest("annotation predicate is required".to_owned())),
_ => Err(ApiError::NotFound(format!("unknown field: {field}"))),
}
}
fn format_field_value(field: &str, object: &str) -> FieldValue {
let is_iri = (field == "wasDerivedFrom"
|| field == "type"
|| field == "role"
|| object.starts_with("http://")
|| object.starts_with("https://"))
&& field != "title"
&& field != "description";
if is_iri {
FieldValue::Iri(object.to_owned())
} else {
FieldValue::Literal(object.to_owned())
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserObjectPath {
pub user_id: String,
pub collection_id: String,
pub display_id: String,
pub version: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicObjectPath {
pub collection_id: String,
pub display_id: String,
pub version: String,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct MemberForm {
pub member: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct AddToCollectionForm {
pub collections: Option<String>,
}
pub async fn user_add_to_collection(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(p): Path<UserObjectPath>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
let object = format!(
"{}user/{}/{}/{}/{}",
state.app.registry_namespace.database_prefix(),
p.user_id,
p.collection_id,
p.display_id,
p.version
);
add_to_collections(state, user, object, &headers, &body).await
}
pub async fn user_remove_membership(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(p): Path<UserObjectPath>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
let collection = format!(
"{}user/{}/{}/{}/{}",
state.app.registry_namespace.database_prefix(),
p.user_id,
p.collection_id,
p.display_id,
p.version
);
membership(state, user, collection, false, &headers, &body).await
}
pub async fn public_add_to_collection(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(p): Path<PublicObjectPath>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
let object = format!(
"{}public/{}/{}/{}",
state.app.registry_namespace.database_prefix(),
p.collection_id,
p.display_id,
p.version
);
add_to_collections(state, user, object, &headers, &body).await
}
pub async fn public_remove_membership(
State(state): State<AppState>,
Extension(CurrentUser(user)): Extension<CurrentUser>,
Path(p): Path<PublicObjectPath>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, ApiError> {
let collection = format!(
"{}public/{}/{}/{}",
state.app.registry_namespace.database_prefix(),
p.collection_id,
p.display_id,
p.version
);
membership(state, user, collection, false, &headers, &body).await
}
async fn membership(
state: AppState,
user: Option<sbol_db_core::User>,
collection: String,
add: bool,
headers: &HeaderMap,
body: &[u8],
) -> Result<Response, ApiError> {
debug_assert!(!add, "addToCollection is served by add_to_collections");
let user = require_user(user)?;
let form = parse_body::<MemberForm>(headers, body)?;
let member = normalize_member(&state, required(form.member, "member")?);
service(&state)
.remove_membership(&user.graph_uri, user.is_admin, &collection, &member)
.await?;
Ok(success())
}
async fn add_to_collections(
state: AppState,
user: Option<sbol_db_core::User>,
object: String,
headers: &HeaderMap,
body: &[u8],
) -> Result<Response, ApiError> {
let user = require_user(user)?;
let form = parse_body::<AddToCollectionForm>(headers, body)?;
let svc = service(&state);
if let Some(collections) = form.collections.filter(|s| !s.is_empty()) {
for target in collections
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
{
let target = normalize_member(&state, target.to_owned());
svc.add_to_collection(&user.graph_uri, user.is_admin, &target, &object)
.await?;
}
}
Ok(success())
}
fn normalize_member(state: &AppState, member: String) -> String {
let base = state.app.registry_namespace.database_prefix();
if let Some(rest) = member.strip_prefix("/public/") {
format!("{base}public/{rest}")
} else if let Some(rest) = member.strip_prefix("/user/") {
format!("{base}user/{rest}")
} else {
member
}
}
fn value_of(value: &Option<String>) -> String {
value.clone().unwrap_or_default()
}
fn required(value: Option<String>, field: &str) -> Result<String, ApiError> {
value
.filter(|s| !s.is_empty())
.ok_or_else(|| ApiError::BadRequest(format!("{field} is required")))
}
fn parse_citations(raw: Option<&str>) -> Result<Vec<String>, ApiError> {
let raw = raw.unwrap_or("").trim();
if raw.is_empty() {
return Ok(Vec::new());
}
let mut ids = Vec::new();
for part in raw.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
if !part.bytes().all(|b| b.is_ascii_digit()) {
return Err(ApiError::BadRequest(
"citations must be comma-separated PubMed ids".to_owned(),
));
}
ids.push(part.to_owned());
}
Ok(ids)
}
fn parse_body<T: for<'de> Deserialize<'de> + Default>(
headers: &HeaderMap,
body: &[u8],
) -> Result<T, ApiError> {
if body.is_empty() {
return Ok(T::default());
}
let is_json = headers
.get(axum::http::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.map(|v| v.starts_with("application/json"))
.unwrap_or(false);
if is_json {
serde_json::from_slice(body)
.map_err(|e| ApiError::BadRequest(format!("invalid JSON body: {e}")))
} else {
serde_urlencoded::from_bytes(body)
.map_err(|e| ApiError::BadRequest(format!("invalid form body: {e}")))
}
}