use std::collections::{HashMap, HashSet};
use topcoat::{
Result,
context::Cx,
router::{Body, error::forbidden},
view::BoxView,
};
use super::{
super::{actions::load_editable, gate::gate},
render::{FormChrome, render_form_page},
};
use crate::{
db::db,
form::{FieldErrors, FormValues, RecordForm},
policy::Ability,
resource::{Mounted, Resource},
topcoat_compat::async_page,
};
pub(crate) struct FormParts {
pub(crate) pairs: Vec<(String, String)>,
pub(crate) files: HashMap<String, crate::upload::StagedUpload>,
pub(crate) file_part_names: HashSet<String>,
}
impl FormParts {
pub(crate) fn last(&self, key: &str) -> Option<&str> {
self.pairs
.iter()
.rev()
.find(|(posted, _)| posted == key)
.map(|(_, value)| value.as_str())
}
}
pub(crate) const MAX_FORM_BYTES: usize = 10 * 1024 * 1024;
pub(super) fn reject_unknown_form_keys(
schema: &crate::schema::Schema,
unclaimed: Vec<String>,
) -> Result<(), topcoat::Error> {
let unknown: Vec<String> = unclaimed
.into_iter()
.filter(|key| {
let file = key
.strip_prefix("clear_")
.or_else(|| key.strip_prefix("keep_"));
key != crate::csrf::FIELD_NAME
&& !file
.is_some_and(|file| schema.fields().any(|f| f.is_file() && f.name() == file))
})
.collect();
if unknown.is_empty() {
Ok(())
} else {
Err(topcoat::router::error::bad_request(format!(
"unknown field(s): {}",
unknown.join(", ")
))
.into())
}
}
pub(crate) fn truthy(v: &str) -> bool {
v == "1" || v == "true"
}
#[derive(Debug, Default)]
pub(super) struct FileFlags {
pub(super) keep: Option<String>,
pub(super) clear: bool,
}
pub(super) fn file_flags(
schema: &crate::schema::Schema,
parts: &FormParts,
) -> HashMap<String, FileFlags> {
schema
.fields()
.filter(|field| field.is_file())
.map(|field| {
let name = field.name();
let flags = FileFlags {
keep: parts.last(&format!("keep_{name}")).map(str::to_string),
clear: parts.last(&format!("clear_{name}")).is_some_and(truthy),
};
(name.to_string(), flags)
})
.collect()
}
pub(super) fn drop_client_typed_uploads(
schema: &crate::schema::Schema,
file_part_names: &HashSet<String>,
values: &mut FormValues,
) {
for field in schema.fields().filter(|field| field.is_file()) {
if !file_part_names.contains(field.name()) {
values.remove(field.name());
}
}
}
pub(super) fn blank(values: &FormValues, key: &str) -> bool {
values.text(key).is_none_or(|value| value.trim().is_empty())
}
pub(super) async fn restore_pending_uploads(
cx: &Cx,
flags: &HashMap<String, FileFlags>,
values: &mut FormValues,
) -> HashSet<String> {
let mut restored = HashSet::new();
for (name, flags) in flags {
if !blank(values, name) || flags.clear {
continue;
}
let Some(candidate) = flags.keep.as_deref().map(str::trim) else {
continue;
};
if candidate.is_empty() || !crate::upload::holds(cx, candidate).await {
continue;
}
values.insert(name.clone(), candidate);
restored.insert(name.clone());
}
restored
}
pub(super) async fn rerender_invalid_form<'a, R: Resource>(
cx: &'a Cx,
resource: &Mounted<R>,
tx: toasty::Transaction<'_>,
chrome: FormChrome,
values: &FormValues,
errors: &FieldErrors,
carried: &HashSet<String>,
) -> Result<BoxView<'a>> {
drop(tx);
render_form_page(cx, &resource.form, chrome, values, errors, carried).await
}
pub(crate) fn resource_edit<R: Resource>(cx: &Cx, _body: Body) -> BoxView<'_> {
async_page(async move {
let resource = gate::<R>(cx)?;
let mut db = db(cx);
let record = load_editable(cx, &resource, &mut db).await?;
if !resource.can(cx, Ability::Update(&record)) {
return Err(forbidden().into());
}
crate::csrf::ensure_token(cx);
let values = <R::Form as RecordForm>::hydrate(cx, &record);
let html = render_form_page(
cx,
&resource.form,
FormChrome::edit(cx, &resource, &record),
&values,
&FieldErrors::new(),
&HashSet::new(),
)
.await?;
Ok(html)
})
}
#[cfg(test)]
mod tests;