use std::{
collections::{HashMap, HashSet},
fmt::Debug,
hash::Hash,
};
use toasty::{Executor, schema::Model};
use topcoat::context::Cx;
use crate::{
detail::Detail,
schema::{FieldResolver, OptionSource, Schema, TypedValue},
table::{Table, column::Includes},
};
#[diagnostic::on_unimplemented(
message = "`{Self}` is not a form scalar",
label = "a form field of this type has no text spelling",
note = "a form scalar is `String`, a `TypedValue` type, an `Options` enum, or an `Option` of \
one; implement `TypedValue` for an app type, derive `Options` for a unit enum, or \
mark an `EmbeddedForm` value `#[form(embed)]`"
)]
pub trait FormScalar: Sized {
const INPUT_TYPE: &'static str = "text";
fn parse_form(value: &str) -> std::result::Result<Self, String>;
fn to_form(&self) -> String;
fn to_label(&self) -> String {
self.to_form()
}
}
impl FormScalar for String {
fn parse_form(value: &str) -> std::result::Result<Self, String> {
Ok(value.to_string())
}
fn to_form(&self) -> String {
self.clone()
}
}
impl<T: TypedValue> FormScalar for T {
const INPUT_TYPE: &'static str = T::INPUT_TYPE;
fn parse_form(value: &str) -> std::result::Result<Self, String> {
T::parse_input(value).ok_or_else(|| format!("`{value}` is not a valid {}", T::NOUN))
}
fn to_form(&self) -> String {
self.to_string()
}
}
#[doc(hidden)]
pub trait NullableScalar: FormScalar {}
impl<T: TypedValue> NullableScalar for T {}
impl<T: NullableScalar> FormScalar for Option<T> {
const INPUT_TYPE: &'static str = T::INPUT_TYPE;
fn parse_form(value: &str) -> std::result::Result<Self, String> {
T::parse_form(value).map(Some)
}
fn to_form(&self) -> String {
self.as_ref().map(T::to_form).unwrap_or_default()
}
fn to_label(&self) -> String {
self.as_ref().map(T::to_label).unwrap_or_default()
}
}
impl FormScalar for Option<String> {
fn parse_form(value: &str) -> std::result::Result<Self, String> {
Ok(Some(value.to_string()))
}
fn to_form(&self) -> String {
self.clone().unwrap_or_default()
}
}
#[doc(hidden)]
pub fn assert_form_scalar<T: FormScalar>() {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum FieldErrorKind {
Required,
Invalid(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldError<K = String> {
pub key: K,
pub kind: FieldErrorKind,
}
impl<K> FieldError<K> {
pub fn required(key: impl Into<K>) -> Self {
Self {
key: key.into(),
kind: FieldErrorKind::Required,
}
}
pub fn invalid(key: impl Into<K>, message: impl Into<String>) -> Self {
Self {
key: key.into(),
kind: FieldErrorKind::Invalid(message.into()),
}
}
pub fn message(&self, label: &str) -> String {
match &self.kind {
FieldErrorKind::Required => format!("{label} is required"),
FieldErrorKind::Invalid(message) => message.clone(),
}
}
}
pub fn parse_scalar<T: FormScalar>(
key: &str,
values: &HashMap<String, String>,
blank: Option<T>,
) -> std::result::Result<T, FieldError> {
let raw = values.get(key).map(|value| value.trim()).unwrap_or("");
if raw.is_empty() {
return blank.ok_or_else(|| FieldError::required(key));
}
T::parse_form(raw).map_err(|message| FieldError::invalid(key, message))
}
pub fn parse_password(
key: &str,
values: &HashMap<String, String>,
blank: Option<String>,
) -> std::result::Result<String, FieldError> {
parse_scalar::<String>(key, values, blank)?;
Ok(values.get(key).cloned().unwrap_or_default())
}
pub fn write_list<'a, T: FormScalar + 'a>(values: impl IntoIterator<Item = &'a T>) -> String {
encode_list(&values.into_iter().map(T::to_form).collect::<Vec<_>>())
}
pub fn parse_list<T: FormScalar>(
key: &str,
values: &HashMap<String, String>,
) -> std::result::Result<Vec<T>, FieldError> {
let Some(items) = decode_list(values.get(key).map_or("", String::as_str)) else {
return Err(FieldError::invalid(key, "The choices could not be read"));
};
items
.iter()
.map(|item| T::parse_form(item).map_err(|message| FieldError::invalid(key, message)))
.collect()
}
pub(crate) fn encode_list(items: &[String]) -> String {
serde_json::to_string(items).expect("a list of strings serializes")
}
pub(crate) fn decode_list(value: &str) -> Option<Vec<String>> {
if value.trim().is_empty() {
return Some(Vec::new());
}
serde_json::from_str(value).ok()
}
#[doc(hidden)]
pub fn links_key<M, R, K>(
path: impl Into<toasty::stmt::Path<M, toasty::stmt::List<R::Model>>>,
) -> String
where
M: Model,
R: OptionSource,
R::Model: Model<PrimaryKey = K>,
K: FormScalar,
{
crate::schema::form_key(path.into())
}
#[doc(hidden)]
pub fn links_include<M, R>(
includes: Includes<M>,
path: impl Into<toasty::stmt::Path<M, toasty::stmt::List<R::Model>>>,
) -> Includes<M>
where
M: Model,
R: OptionSource,
{
includes.with(path.into())
}
#[doc(hidden)]
pub fn write_links<T>(records: &toasty::Deferred<Vec<T>>) -> Option<String>
where
T: Model + toasty::stmt::IntoExpr<T>,
{
(!records.is_unloaded()).then(|| {
encode_list(
&records
.get()
.iter()
.map(crate::toasty_compat::pk::pk_text)
.collect::<Vec<_>>(),
)
})
}
#[derive(Debug, Clone)]
pub struct FormField<K> {
pub field: K,
pub name: &'static str,
pub keys: Vec<String>,
pub required: Vec<String>,
}
pub trait RecordForm: Sized + Send + 'static {
type Model: Model + toasty::stmt::IntoExpr<Self::Model> + Send + Sync + 'static;
type Field: Copy + Eq + Hash + Debug + Send + Sync + 'static;
fn fields(resolver: &FieldResolver) -> Vec<FormField<Self::Field>>;
fn control(field: Self::Field) -> Schema<Self>;
fn table() -> Table<Self::Model> {
Table::new(())
}
fn detail() -> Detail<Self::Model> {
Detail::empty()
}
fn includes() -> Includes<Self::Model> {
Includes::new()
}
fn hydrate(cx: &Cx, record: &Self::Model) -> HashMap<String, String>;
fn links(&self) -> Vec<(Self::Field, Vec<String>)> {
Vec::new()
}
fn parse(
cx: &Cx,
values: &HashMap<String, String>,
) -> std::result::Result<Self, Vec<FieldError>>;
fn into_create(self) -> <Self::Model as Model>::Create;
fn into_update<'a>(
self,
record: &'a mut Self::Model,
named: &HashSet<Self::Field>,
) -> Option<<Self::Model as Model>::Update<'a>>;
fn exec_update<'a>(
update: <Self::Model as Model>::Update<'a>,
ex: &'a mut dyn Executor,
) -> impl Future<Output = toasty::Result<()>> + Send + 'a;
const HAS_FORM: bool = true;
}
pub struct NoForm<M>(std::marker::PhantomData<fn() -> M>);
impl<M: Model + toasty::stmt::IntoExpr<M> + Send + Sync + 'static> NoForm<M> {
fn unreachable() -> ! {
panic!("`NoForm<{}>` has no form", std::any::type_name::<M>())
}
}
impl<M: Model + toasty::stmt::IntoExpr<M> + Send + Sync + 'static> RecordForm for NoForm<M> {
type Model = M;
type Field = std::convert::Infallible;
const HAS_FORM: bool = false;
fn fields(_resolver: &FieldResolver) -> Vec<FormField<Self::Field>> {
Vec::new()
}
fn control(field: Self::Field) -> Schema<Self> {
match field {}
}
fn hydrate(_cx: &Cx, _record: &M) -> HashMap<String, String> {
HashMap::new()
}
fn parse(
_cx: &Cx,
_values: &HashMap<String, String>,
) -> std::result::Result<Self, Vec<FieldError>> {
Self::unreachable()
}
fn into_create(self) -> M::Create {
Self::unreachable()
}
fn into_update<'a>(
self,
_record: &'a mut M,
_named: &HashSet<Self::Field>,
) -> Option<M::Update<'a>> {
None
}
fn exec_update<'a>(
update: M::Update<'a>,
ex: &'a mut dyn Executor,
) -> impl Future<Output = toasty::Result<()>> + Send + 'a {
drop((update, ex));
async { Self::unreachable() }
}
}
pub struct Posted<F: RecordForm> {
form: F,
named: HashSet<F::Field>,
}
impl<F: RecordForm> Posted<F> {
pub fn new(form: F, named: impl IntoIterator<Item = F::Field>) -> Self {
Self {
form,
named: named.into_iter().collect(),
}
}
pub fn named(&self, field: F::Field) -> bool {
self.named.contains(&field)
}
pub fn into_update(self, record: &mut F::Model) -> Option<<F::Model as Model>::Update<'_>> {
self.form.into_update(record, &self.named)
}
}
impl<F: RecordForm> std::ops::Deref for Posted<F> {
type Target = F;
fn deref(&self) -> &F {
&self.form
}
}
#[derive(Debug)]
pub struct FieldErrors<K = String> {
errors: Vec<FieldError<K>>,
}
impl<K> Default for FieldErrors<K> {
fn default() -> Self {
Self { errors: Vec::new() }
}
}
impl<K> FieldErrors<K> {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, key: impl Into<K>, message: impl Into<String>) {
self.errors.push(FieldError::invalid(key, message));
}
pub fn push(&mut self, error: FieldError<K>) {
self.errors.push(error);
}
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &FieldError<K>> {
self.errors.iter()
}
pub fn extend(&mut self, other: Self) {
self.errors.extend(other.errors);
}
}
impl<K> IntoIterator for FieldErrors<K> {
type Item = FieldError<K>;
type IntoIter = std::vec::IntoIter<FieldError<K>>;
fn into_iter(self) -> Self::IntoIter {
self.errors.into_iter()
}
}
impl FieldErrors {
pub fn contains_key(&self, key: &str) -> bool {
self.errors.iter().any(|error| error.key == key)
}
pub fn first(&self, key: &str) -> Option<&FieldError> {
self.errors.iter().find(|error| error.key == key)
}
pub(crate) fn replace(&mut self, other: Self) {
let owned: HashSet<&str> = other
.errors
.iter()
.map(|error| error.key.as_str())
.collect();
self.errors
.retain(|kept| !owned.contains(kept.key.as_str()));
self.errors.extend(other.errors);
}
}
#[cfg(test)]
mod tests;