use std::{
collections::{HashMap, HashSet},
fmt::Debug,
hash::Hash,
};
use toasty::{Executor, schema::Model, stmt::IntoInsert};
use topcoat::context::Cx;
use crate::{
schema::{Schema, TypedValue},
table::Table,
};
#[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, or an `Option` of one; implement \
`TypedValue` for an app type, or mark an `EmbeddedForm` value `#[form(embed)]`"
)]
pub trait FormScalar: Sized {
const INPUT_TYPE: &'static str = "text";
fn blank() -> Option<Self>;
fn parse_form(value: &str) -> std::result::Result<Self, String>;
fn to_form(&self) -> String;
}
impl FormScalar for String {
fn blank() -> Option<Self> {
Some(String::new())
}
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 blank() -> Option<Self> {
None
}
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()
}
}
impl<T: TypedValue> FormScalar for Option<T> {
const INPUT_TYPE: &'static str = T::INPUT_TYPE;
fn blank() -> Option<Self> {
Some(None)
}
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()
}
}
impl FormScalar for Option<String> {
fn blank() -> Option<Self> {
Some(None)
}
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, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FieldErrorKind {
Required,
Invalid,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldError {
pub key: String,
pub kind: FieldErrorKind,
pub message: String,
}
impl FieldError {
pub fn unanswered(key: impl Into<String>, message: impl Into<String>) -> Self {
Self {
key: key.into(),
kind: FieldErrorKind::Required,
message: message.into(),
}
}
pub fn required(key: impl Into<String>) -> Self {
let key = key.into();
let message = format!("{key} is required");
Self::unanswered(key, message)
}
pub fn invalid(key: impl Into<String>, message: impl Into<String>) -> Self {
Self {
key: key.into(),
kind: FieldErrorKind::Invalid,
message: message.into(),
}
}
}
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
.or_else(T::blank)
.ok_or_else(|| FieldError::required(key));
}
T::parse_form(raw).map_err(|message| FieldError::invalid(key, message))
}
#[derive(Debug, Clone)]
pub struct FormField<K> {
pub field: K,
pub name: &'static str,
pub keys: Vec<String>,
pub answers_blank: bool,
}
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() -> Vec<FormField<Self::Field>>;
fn schema() -> Schema {
Schema::empty()
}
fn table() -> Table<Self::Model> {
Table::new(())
}
fn hydrate(cx: &Cx, record: &Self::Model) -> HashMap<String, String>;
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() -> Vec<FormField<Self::Field>> {
Vec::new()
}
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, Default)]
pub struct FieldErrors {
errors: Vec<FieldError>,
}
impl FieldErrors {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, key: impl Into<String>, message: impl Into<String>) {
self.errors.push(FieldError::invalid(key, message));
}
pub fn add_required(&mut self, key: impl Into<String>, message: impl Into<String>) {
self.errors.push(FieldError::unanswered(key, message));
}
pub fn push(&mut self, error: FieldError) {
self.errors.push(error);
}
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 fn is_empty(&self) -> bool {
self.errors.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &FieldError> {
self.errors.iter()
}
pub fn extend(&mut self, other: Self) {
self.errors.extend(other.errors);
}
pub 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);
}
}
pub(crate) fn prefilled_fields<M: Model>() -> Vec<bool> {
let insert = <M::Create as Default>::default().into_insert();
let toasty_core::stmt::Expr::Stmt(statement) = toasty_core::stmt::Expr::from(insert) else {
return Vec::new();
};
statement
.stmt
.as_insert()
.and_then(|insert| insert.source.body.as_values())
.and_then(|values| values.rows.last())
.and_then(|row| row.as_record())
.map(|record| {
record
.fields
.iter()
.map(|expr| !expr.is_value_null())
.collect()
})
.unwrap_or_default()
}