use std::{
collections::{HashMap, HashSet},
fmt::Debug,
hash::Hash,
};
use toasty::{Executor, schema::Model};
use topcoat::context::Cx;
use crate::{
schema::{FieldResolver, 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 parse_form(value: &str) -> std::result::Result<Self, String>;
fn to_form(&self) -> String;
}
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()
}
}
impl<T: TypedValue> 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()
}
}
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))
}
#[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 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(_resolver: &FieldResolver) -> 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)]
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);
}
}