use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use topcoat::{
Result,
context::Cx,
router::{Body, error::forbidden},
view::BoxView,
};
use super::{
super::{actions::find_by_key, gate::gate, write::commit_write},
common::{
FormParts, drop_client_typed_uploads, reject_unknown_form_keys, rerender_invalid_form,
restore_pending_uploads, strip_transport_keys, truthy,
},
decode::parse_form_body,
render::FormChrome,
unique::check_unique,
};
use crate::{
db::db,
form::{FieldErrorKind, FieldErrors, Posted, RecordForm},
policy::Ability,
resource::{Committed, Mounted, Resource},
schema::Schema,
topcoat_compat::async_page,
};
const WRITE_CREATE: &str = "create the record";
const WRITE_UPDATE: &str = "save the changes";
struct Submission {
schema: Arc<Schema>,
values: HashMap<String, String>,
errors: FieldErrors,
carried: HashSet<String>,
named: HashSet<String>,
}
async fn prepare_submission<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
parts: FormParts,
advisory: Option<&R::Model>,
) -> Result<Submission, topcoat::Error> {
let schema = Arc::clone(&resource.form);
reject_unknown_form_keys(&schema, &parts.values)?;
let FormParts {
mut values,
files,
file_part_names,
} = parts;
let stored = advisory
.map(|advisory| <R::Form as RecordForm>::hydrate(cx, advisory))
.unwrap_or_default();
drop_client_typed_uploads(&schema, &file_part_names, &mut values);
let (upload_errors, mut carried) =
crate::upload::store_uploads(cx, &schema, &files, &mut values).await;
carried.extend(restore_pending_uploads(cx, &schema, &mut values).await);
for field in schema.fields().filter(|field| field.is_file()) {
let name = field.name();
let cleared = values
.get(&format!("clear_{name}"))
.is_some_and(|v| truthy(v));
let empty = values
.get(name)
.map(|v| v.trim().is_empty())
.unwrap_or(true);
if !cleared && empty && stored.get(name).is_some_and(|v| !v.trim().is_empty()) {
values.remove(name);
}
}
strip_transport_keys(&schema, &mut values);
let named: HashSet<String> = values.keys().cloned().collect();
complete(&schema, &mut values, &named, &stored);
let mut errors = schema.validate_async(cx, &values).await;
errors.replace(upload_errors);
Ok(Submission {
schema,
values,
errors,
carried,
named,
})
}
fn complete(
schema: &Schema,
values: &mut HashMap<String, String>,
named: &HashSet<String>,
stored: &HashMap<String, String>,
) {
for field in schema.fields() {
let name = field.name();
if named.contains(name) {
continue;
}
match stored.get(name) {
Some(value) => values.insert(name.to_string(), value.clone()),
None => values.remove(name),
};
}
}
fn parse_form<R: Resource>(
cx: &Cx,
schema: &Schema,
values: &HashMap<String, String>,
errors: &mut FieldErrors,
) -> Result<Option<R::Form>, topcoat::Error> {
let mut normalized = values.clone();
schema.normalize_values(&mut normalized);
match <R::Form as RecordForm>::parse(cx, &normalized) {
Ok(form) => {
for error in R::validate_record(cx, &form).iter() {
if !schema.renders_error_key(values, &error.key) {
return Err(unrenderable_error::<R>(
"validate_record",
&error.key,
&error.message,
));
}
errors.push(error.clone());
}
Ok(Some(form))
}
Err(failures) => {
let controls = schema.controls();
for mut failure in failures {
if !schema.renders_error_key(values, &failure.key) {
return Err(unrenderable_error::<R>(
"the parse",
&failure.key,
&failure.message,
));
}
if errors.contains_key(&failure.key) {
continue;
}
if failure.kind == FieldErrorKind::Required
&& let Some(wording) = controls
.iter()
.find(|control| control.name == failure.key)
.and_then(|control| control.required_error.clone())
{
failure.message = wording;
}
errors.push(failure);
}
Ok(None)
}
}
}
async fn recheck_relationships(
cx: &Cx,
schema: &Schema,
values: &HashMap<String, String>,
errors: &mut FieldErrors,
ex: &mut dyn toasty::Executor,
) {
if errors.is_empty() {
errors.extend(schema.recheck_relationships(cx, values, ex).await);
}
}
fn unrenderable_error<R: Resource>(source: &str, key: &str, message: &str) -> topcoat::Error {
crate::error::declaration(format!(
"{source} refused {key:?}, which `{}` renders nowhere for this submission: {message}",
std::any::type_name::<R::Form>()
))
}
fn named_fields<R: Resource>(
resource: &Mounted<R>,
named: &HashSet<String>,
) -> Vec<<R::Form as RecordForm>::Field> {
resource
.fields
.iter()
.filter(|field| field.keys.iter().any(|key| named.contains(key)))
.map(|field| field.field)
.collect()
}
pub(crate) fn resource_create_post<R: Resource>(cx: &Cx, body: Body) -> BoxView<'_> {
async_page(async move {
let resource = gate::<R>(cx)?;
if !resource.can(cx, Ability::Create) {
return Err(forbidden().into());
}
let parts = parse_form_body(cx, body).await?;
crate::csrf::verify(cx, &parts.values)?;
let Submission {
schema,
values,
mut errors,
carried,
..
} = prepare_submission(cx, &resource, parts, None).await?;
let mut db = db(cx);
let mut tx = db.transaction().await.map_err(crate::error::unavailable)?;
errors
.extend(check_unique(cx, &resource, &schema, &values, &HashMap::new(), &mut tx).await?);
let form = parse_form::<R>(cx, &schema, &values, &mut errors)?;
recheck_relationships(cx, &schema, &values, &mut errors, &mut tx).await;
let Some(form) = form.filter(|_| errors.is_empty()) else {
return rerender_invalid_form(
cx,
&resource,
tx,
FormChrome::create(&resource),
&values,
&errors,
&carried,
)
.await;
};
let written = R::create_record(cx, form, &mut tx).await;
commit_write(
cx,
&resource,
tx,
written,
Committed::created,
"Created",
WRITE_CREATE,
)
.await
})
}
pub(crate) fn resource_edit_post<R: Resource>(cx: &Cx, body: Body) -> BoxView<'_> {
async_page(async move {
let resource = gate::<R>(cx)?;
let parts = parse_form_body(cx, body).await?;
crate::csrf::verify(cx, &parts.values)?;
let id = topcoat::router::path_param_segment(cx, "id").to_string();
let mut db0 = db(cx);
let advisory = find_by_key(cx, &resource, &id, &mut db0).await?;
if !resource.can(cx, Ability::View(&advisory)) {
return Err(forbidden().into());
}
if !resource.can(cx, Ability::Update(&advisory)) {
return Err(forbidden().into());
}
let Submission {
schema,
mut values,
mut errors,
carried,
named,
} = prepare_submission(cx, &resource, parts, Some(&advisory)).await?;
let mut db = db(cx);
let mut tx = db.transaction().await.map_err(crate::error::unavailable)?;
let record = find_by_key(cx, &resource, &id, &mut tx).await?;
if !resource.can(cx, Ability::View(&record)) {
return Err(forbidden().into());
}
if !resource.can(cx, Ability::Update(&record)) {
return Err(forbidden().into());
}
let stored = <R::Form as RecordForm>::hydrate(cx, &record);
complete(&schema, &mut values, &named, &stored);
errors.extend(check_unique(cx, &resource, &schema, &values, &stored, &mut tx).await?);
let form = parse_form::<R>(cx, &schema, &values, &mut errors)?;
recheck_relationships(cx, &schema, &values, &mut errors, &mut tx).await;
let Some(form) = form.filter(|_| errors.is_empty()) else {
return rerender_invalid_form(
cx,
&resource,
tx,
FormChrome::edit(cx, &resource, &record),
&values,
&errors,
&carried,
)
.await;
};
let posted = Posted::new(form, named_fields(&resource, &named));
let written = R::update_record(cx, record, posted, &mut tx).await;
commit_write(
cx,
&resource,
tx,
written,
Committed::updated,
"Updated",
WRITE_UPDATE,
)
.await
})
}
#[cfg(test)]
mod tests;