use axum::body::Bytes;
use axum::extract::{FromRequest, Multipart, Path, Request, State};
use axum::http::header::{CONTENT_TYPE, LOCATION};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Extension;
use axum::Json;
use sbol_db_app::SubmitRequest;
use sbol_db_core::{IriString, User};
use serde::Deserialize;
use serde_json::json;
use super::auth::{require_user, Identity};
use super::util::{
encode_iri_segment, parse_citations, required, resolve_overwrite, resolve_submission_format,
};
use crate::error::ApiError;
use crate::v2::error::V2Error;
use crate::AppState;
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct CreateForm {
id: Option<String>,
version: Option<String>,
name: Option<String>,
description: Option<String>,
citations: Vec<String>,
creator_name: Option<String>,
format: Option<String>,
overwrite: Option<String>,
content: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
struct MemberBody {
member: Option<String>,
}
pub async fn create_collection(
State(state): State<AppState>,
Extension(identity): Extension<Identity>,
request: Request,
) -> Result<Response, V2Error> {
let user = require_contributor(&identity)?;
let form = parse_create_form(request, &state).await?;
let request = submit_request(form, &user)?;
let outcome = state.app.submission_service().submit(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(),
"persistent_identity": outcome.collection_persistent_identity.as_str(),
"members": members,
"graph": outcome.graph_iri,
"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())
}
pub async fn validate_collection(
State(state): State<AppState>,
Extension(identity): Extension<Identity>,
request: Request,
) -> Result<Response, V2Error> {
let user = require_contributor(&identity)?;
let form = parse_create_form(request, &state).await?;
let request = submit_request(form, &user)?;
let preview = state.app.submission_service().preview(&request).await?;
Ok(Json(preview).into_response())
}
pub async fn add_member(
State(state): State<AppState>,
Extension(identity): Extension<Identity>,
Path(collection): Path<String>,
body: Bytes,
) -> Result<StatusCode, V2Error> {
let user = require_contributor(&identity)?;
let form: MemberBody = super::util::parse_json(&body)?;
let member = required(form.member, "member")?;
validate_iri(&collection, "collection")?;
validate_iri(&member, "member")?;
state
.app
.edit_service()
.add_to_collection(&user.graph_uri, user.is_admin, &collection, &member)
.await?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn remove_member(
State(state): State<AppState>,
Extension(identity): Extension<Identity>,
Path((collection, member)): Path<(String, String)>,
) -> Result<StatusCode, V2Error> {
let user = require_contributor(&identity)?;
validate_iri(&collection, "collection")?;
validate_iri(&member, "member")?;
state
.app
.edit_service()
.remove_membership(&user.graph_uri, user.is_admin, &collection, &member)
.await?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn delete_collection(
State(state): State<AppState>,
Extension(identity): Extension<Identity>,
Path(collection): Path<String>,
) -> Result<StatusCode, V2Error> {
let user = require_contributor(&identity)?;
validate_iri(&collection, "collection")?;
state
.app
.mutation_service()
.remove_collection(&user.graph_uri, user.is_admin, &collection)
.await?;
Ok(StatusCode::NO_CONTENT)
}
fn validate_iri(value: &str, field: &str) -> Result<(), V2Error> {
IriString::new(value.to_owned())
.map(|_| ())
.map_err(|error| {
V2Error::from(ApiError::BadRequest(format!(
"invalid {field} IRI: {error}"
)))
})
}
fn require_contributor(identity: &Identity) -> Result<User, V2Error> {
let user = require_user(identity)?;
if !user.is_member && !user.is_admin {
return Err(V2Error::from(ApiError::Forbidden(
"an active member account is required to contribute".to_owned(),
)));
}
Ok(user)
}
async fn parse_create_form(request: Request, state: &AppState) -> Result<CreateForm, V2Error> {
let content_type = request
.headers()
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.unwrap_or("")
.to_owned();
if content_type.starts_with("multipart/form-data") {
let multipart = Multipart::from_request(request, state)
.await
.map_err(|error| {
V2Error::from(ApiError::BadRequest(format!(
"invalid multipart body: {error}"
)))
})?;
parse_multipart(multipart).await
} else {
let bytes = Bytes::from_request(request, state).await.map_err(|error| {
V2Error::from(ApiError::BadRequest(format!(
"invalid request body: {error}"
)))
})?;
super::util::parse_json(&bytes)
}
}
fn submit_request(form: CreateForm, user: &User) -> Result<SubmitRequest, V2Error> {
Ok(SubmitRequest {
owner: user.username.clone(),
id: required(form.id, "id")?,
version: required(form.version, "version")?,
name: form.name.filter(|value| !value.is_empty()),
description: form.description.filter(|value| !value.is_empty()),
creator_name: form
.creator_name
.filter(|value| !value.is_empty())
.or_else(|| Some(user.name.clone())),
citations: form.citations,
body: required(form.content, "content")?,
format: resolve_submission_format(form.format.as_deref())?,
overwrite: resolve_overwrite(form.overwrite.as_deref())?,
})
}
async fn parse_multipart(mut multipart: Multipart) -> Result<CreateForm, V2Error> {
let mut form = CreateForm::default();
while let Some(field) = multipart.next_field().await.map_err(|e| {
V2Error::from(ApiError::BadRequest(format!(
"malformed multipart body: {e}"
)))
})? {
let name = field.name().map(str::to_owned);
match name.as_deref() {
Some("file") | Some("content") => form.content = Some(field_text(field).await?),
Some("id") => form.id = Some(field_text(field).await?),
Some("version") => form.version = Some(field_text(field).await?),
Some("name") => form.name = Some(field_text(field).await?),
Some("description") => form.description = Some(field_text(field).await?),
Some("creator_name") => form.creator_name = Some(field_text(field).await?),
Some("format") => form.format = Some(field_text(field).await?),
Some("overwrite") => form.overwrite = Some(field_text(field).await?),
Some("citations") => form.citations = parse_citations(Some(&field_text(field).await?)),
_ => {
let _ = field.bytes().await;
}
}
}
Ok(form)
}
async fn field_text(field: axum::extract::multipart::Field<'_>) -> Result<String, V2Error> {
field.text().await.map_err(|e| {
V2Error::from(ApiError::BadRequest(format!(
"invalid multipart field: {e}"
)))
})
}