parent
bef1ebdae7
commit
453d36dd86
@ -1,26 +0,0 @@
|
|||||||
use axum::async_trait;
|
|
||||||
use axum::http::StatusCode;
|
|
||||||
use axum::response::IntoResponse;
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use crate::app::routes::AppState;
|
|
||||||
use crate::error::AppError;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait HtmxFormData : DeserializeOwned + Send {
|
|
||||||
type FormTemplate: IntoResponse;
|
|
||||||
|
|
||||||
async fn validate(self, state: &AppState) -> Result<Self, HtmxFormDataError<Self::FormTemplate>>;
|
|
||||||
|
|
||||||
async fn base_template(state: &AppState) -> anyhow::Result<Self::FormTemplate>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum HtmxFormDataError<T> {
|
|
||||||
ValidationError(T),
|
|
||||||
InternalServerError(anyhow::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> From<anyhow::Error> for HtmxFormDataError<T> {
|
|
||||||
fn from(err: anyhow::Error) -> Self {
|
|
||||||
HtmxFormDataError::InternalServerError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
pub mod validated_form;
|
|
||||||
pub mod htmx_form_data;
|
|
||||||
pub mod form_helpers;
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
use axum::extract::{FromRequest, FromRequestParts, Request};
|
|
||||||
use axum::http::StatusCode;
|
|
||||||
use axum::{async_trait, RequestPartsExt};
|
|
||||||
use axum::extract::rejection::FormRejection;
|
|
||||||
use axum::response::{IntoResponse, IntoResponseParts};
|
|
||||||
use crate::app::routes::AppState;
|
|
||||||
use crate::error::AppError;
|
|
||||||
use super::htmx_form_data::{HtmxFormData, HtmxFormDataError};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
|
||||||
pub struct ValidatedForm<T>(pub T);
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<T> FromRequest<AppState> for ValidatedForm<T>
|
|
||||||
where
|
|
||||||
T: HtmxFormData
|
|
||||||
{
|
|
||||||
type Rejection = ValidatedFormError<T::FormTemplate>;
|
|
||||||
|
|
||||||
async fn from_request(req: Request, state: &AppState) -> Result<Self, Self::Rejection> {
|
|
||||||
|
|
||||||
let raw_form_data = axum::Form::<T>::from_request(req, state)
|
|
||||||
.await
|
|
||||||
.map_err(|e| ValidatedFormError::FormError(e))?;
|
|
||||||
|
|
||||||
let value = raw_form_data.0
|
|
||||||
.validate(&state)
|
|
||||||
.await
|
|
||||||
.map_err(|e|
|
|
||||||
match e {
|
|
||||||
HtmxFormDataError::ValidationError(e) => ValidatedFormError::ValidationError(e),
|
|
||||||
HtmxFormDataError::InternalServerError(e) => ValidatedFormError::InternalServerError(e),
|
|
||||||
}
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(ValidatedForm(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Deref for ValidatedForm<T> {
|
|
||||||
type Target = T;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> DerefMut for ValidatedForm<T> {
|
|
||||||
#[inline]
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ValidatedFormError<T> {
|
|
||||||
ValidationError(T),
|
|
||||||
FormError(FormRejection),
|
|
||||||
InternalServerError(anyhow::Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl<T> IntoResponse for ValidatedFormError<T>
|
|
||||||
where T: IntoResponse
|
|
||||||
{
|
|
||||||
fn into_response(self) -> axum::response::Response {
|
|
||||||
match self {
|
|
||||||
ValidatedFormError::FormError(err) => {
|
|
||||||
err.into_response()
|
|
||||||
},
|
|
||||||
ValidatedFormError::ValidationError(err) => {
|
|
||||||
let (mut parts, body) = err.into_response().into_parts();
|
|
||||||
parts.status = StatusCode::OK;
|
|
||||||
(parts, body).into_response()
|
|
||||||
}
|
|
||||||
ValidatedFormError::InternalServerError(err) => {
|
|
||||||
AppError::from(err).into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
use askama_axum::IntoResponse;
|
||||||
|
use axum::async_trait;
|
||||||
|
use crate::app::routes::AppState;
|
||||||
|
use crate::error::AppError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
This is sort of like "Default" but for responses. Some elements need to query the database
|
||||||
|
or some other part of the state in order to respond coherently. For example: a form that
|
||||||
|
requires a database query to populate a dropdown.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait BaseResponse {
|
||||||
|
type ResponseType: IntoResponse;
|
||||||
|
|
||||||
|
async fn base_response(state: &AppState) -> Result<Self::ResponseType, AppError>;
|
||||||
|
}
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
use axum::async_trait;
|
||||||
|
use axum::extract::{FromRequest, FromRequestParts, Request};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use crate::app::routes::AppState;
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::util::form::validate_form::{ValidateForm, ValidateFormError};
|
||||||
|
|
||||||
|
/**
|
||||||
|
Essentially a wrapper around the Form extractor from axum. Needed in order to provide a common
|
||||||
|
language for Form handling (see more complex structures below) and standardizes the
|
||||||
|
error handling by using AppError for rejections.
|
||||||
|
*/
|
||||||
|
pub struct FormBase<F> {
|
||||||
|
pub form_data: F,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<F> FromRequest<AppState> for FormBase<F>
|
||||||
|
where
|
||||||
|
F: DeserializeOwned + Send,
|
||||||
|
{
|
||||||
|
type Rejection = AppError;
|
||||||
|
|
||||||
|
async fn from_request(req: Request, state: &AppState) -> Result<Self, Self::Rejection> {
|
||||||
|
|
||||||
|
let form_data = axum::Form::<F>::from_request(req, state)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Self { form_data: form_data.0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
A slightly more complex Form that contains data from the Path as well as the form body. This layer
|
||||||
|
of wrapping allows for validation in one spot as it can package up more of the information
|
||||||
|
needed to validate values.
|
||||||
|
*/
|
||||||
|
pub struct FormWithPathVars<P, F> {
|
||||||
|
pub path_data: P,
|
||||||
|
pub form_data: F,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<P, F> FromRequest<AppState> for FormWithPathVars<P, F>
|
||||||
|
where
|
||||||
|
P: DeserializeOwned + Send,
|
||||||
|
F: DeserializeOwned + Send,
|
||||||
|
{
|
||||||
|
type Rejection = AppError;
|
||||||
|
|
||||||
|
async fn from_request(req: Request, state: &AppState) -> Result<Self, Self::Rejection> {
|
||||||
|
|
||||||
|
let (mut parts, body) = req.into_parts();
|
||||||
|
|
||||||
|
let path_data = axum::extract::Path::<P>::from_request_parts(&mut parts, state)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let req = Request::from_parts(parts, body);
|
||||||
|
|
||||||
|
let form_data = axum::Form::<F>::from_request(req, state)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Self { path_data: path_data.0, form_data: form_data.0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
An extractor that can take anything that implements ValidateForm and handle validation errors
|
||||||
|
before they get into the body of the handler. Regular application errors will respond with
|
||||||
|
500 errors, but validation errors will respond with 200 and contain the error info
|
||||||
|
*/
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct ValidForm<T>(pub T);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T> FromRequest<AppState> for ValidForm<T>
|
||||||
|
where
|
||||||
|
T: ValidateForm, ValidateFormError<<T as ValidateForm>::ValidationErrorResponse>: From<<T as FromRequest<AppState>>::Rejection>
|
||||||
|
{
|
||||||
|
type Rejection = ValidateFormError<T::ValidationErrorResponse>;
|
||||||
|
|
||||||
|
async fn from_request(req: Request, state: &AppState) -> Result<Self, Self::Rejection> {
|
||||||
|
|
||||||
|
let raw_data = T::from_request(req, state).await?;
|
||||||
|
|
||||||
|
let value = raw_data.validate(&state).await?;
|
||||||
|
|
||||||
|
Ok(ValidForm(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Deref for ValidForm<T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DerefMut for ValidForm<T> {
|
||||||
|
#[inline]
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
/**
|
||||||
|
A common type that can be used as a validation error for a field value in a form.
|
||||||
|
*/
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub enum FieldError {
|
||||||
|
Required,
|
||||||
|
ValidIdentifier,
|
||||||
|
PositiveNumber,
|
||||||
|
WholeNumber,
|
||||||
|
Currency,
|
||||||
|
SelectOption,
|
||||||
|
Custom(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for FieldError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
FieldError::Required => write!(f, "Requires a value"),
|
||||||
|
FieldError::ValidIdentifier => write!(f, "Invalid identifier"),
|
||||||
|
FieldError::PositiveNumber => write!(f, "Requires a positive number"),
|
||||||
|
FieldError::WholeNumber => write!(f, "Requires a whole number"),
|
||||||
|
FieldError::Currency => write!(f, "Requires a dollar amount"),
|
||||||
|
FieldError::SelectOption => write!(f, "Requires a valid selection"),
|
||||||
|
FieldError::Custom(s) => write!(f, "{}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&'static str> for FieldError {
|
||||||
|
fn from(s: &'static str) -> Self {
|
||||||
|
FieldError::Custom(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
use crate::util::form::field_error::FieldError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
A sum type that contains a value and possibly an error. Used for form validation.
|
||||||
|
This isn't exactly like a sum type like Result as even in the error case, we want
|
||||||
|
to keep the value intact. It can't be just an either/or situation. For instance,
|
||||||
|
when doing form validation, in most cases, we would want to preserve the value
|
||||||
|
sent to us through the form, but inform the user of why it was rejected through an
|
||||||
|
error message.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub struct FieldValue<Value, Error> {
|
||||||
|
pub value: Value,
|
||||||
|
pub error: Option<Error>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type StringField = FieldValue<String, FieldError>;
|
||||||
|
pub type BoolField = FieldValue<bool, FieldError>;
|
||||||
|
pub type FloatField = FieldValue<f64, FieldError>;
|
||||||
|
|
||||||
|
impl<Value, Error> FieldValue<Value, Error>
|
||||||
|
where Value: Default
|
||||||
|
{
|
||||||
|
pub fn new(value: Value) -> Self {
|
||||||
|
Self { value, error: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_error(&mut self, error: Error) {
|
||||||
|
self.error = Some(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_error(&self) -> bool {
|
||||||
|
self.error.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invalid_if<I>(mut self, is_invalid: I, err: Error ) -> Self
|
||||||
|
where I: FnOnce(&Value) -> bool {
|
||||||
|
if is_invalid(&self.value) {
|
||||||
|
self.set_error(err);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invalid_if_then<I, E>(mut self, is_invalid: I, err_if: E ) -> Self
|
||||||
|
where I: FnOnce(&Value) -> bool, E: FnOnce() -> Error {
|
||||||
|
if is_invalid(&self.value) {
|
||||||
|
self.set_error(err_if());
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_value(mut self) -> Self {
|
||||||
|
self.value = Default::default();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_value_if_error(mut self) -> Self {
|
||||||
|
if self.is_error() { self.clear_value() } else { self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map<F, V>(self, f: F) -> FieldValue<V, Error>
|
||||||
|
where F: FnOnce(Value) -> V {
|
||||||
|
FieldValue::<V, Error> {
|
||||||
|
value: f(self.value),
|
||||||
|
error: self.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Value, Error> Default for FieldValue<Value, Error>
|
||||||
|
where Value: Default,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(Value::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Value, Error> Debug for FieldValue<Value, Error>
|
||||||
|
where Value: Debug,
|
||||||
|
Error: Debug {
|
||||||
|
fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
|
||||||
|
fmt.debug_struct("FieldValue")
|
||||||
|
.field("value", &self.value)
|
||||||
|
.field("error", &self.error)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,8 @@
|
|||||||
use serde::{Deserialize, Deserializer};
|
use serde::{Deserialize, Deserializer};
|
||||||
|
|
||||||
|
/**
|
||||||
|
Deserialize a checkbox field from a html form into a bool
|
||||||
|
*/
|
||||||
pub fn deserialize_form_checkbox<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
pub fn deserialize_form_checkbox<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
||||||
where D: Deserializer<'de> {
|
where D: Deserializer<'de> {
|
||||||
let buf = String::deserialize(deserializer)?;
|
let buf = String::deserialize(deserializer)?;
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
pub mod field_error;
|
||||||
|
pub mod field_value;
|
||||||
|
pub mod validate_form;
|
||||||
|
pub mod extractors;
|
||||||
|
pub mod helpers;
|
||||||
|
pub mod base_response;
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
use axum::extract::FromRequest;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::{async_trait, RequestPartsExt};
|
||||||
|
use axum::response::{IntoResponse, IntoResponseParts};
|
||||||
|
use crate::app::routes::AppState;
|
||||||
|
use crate::error::AppError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Validate a form. Allows forms to implement one validation function and be automatically hooked
|
||||||
|
up with extractors to reduce boilerplate in endpoints
|
||||||
|
*/
|
||||||
|
#[async_trait]
|
||||||
|
pub trait ValidateForm: FromRequest<AppState> {
|
||||||
|
type ValidationErrorResponse: IntoResponse;
|
||||||
|
|
||||||
|
async fn validate(self, state: &AppState) -> Result<Self, ValidateFormError<Self::ValidationErrorResponse>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub enum ValidateFormError<T> {
|
||||||
|
ValidationError(T),
|
||||||
|
InternalServerError(AppError)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> IntoResponse for ValidateFormError<T>
|
||||||
|
where T: IntoResponse
|
||||||
|
{
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
match self {
|
||||||
|
ValidateFormError::ValidationError(err) => {
|
||||||
|
let (mut parts, body) = err.into_response().into_parts();
|
||||||
|
parts.status = StatusCode::OK;
|
||||||
|
(parts, body).into_response()
|
||||||
|
}
|
||||||
|
ValidateFormError::InternalServerError(err) => {
|
||||||
|
err.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<anyhow::Error> for ValidateFormError<T> {
|
||||||
|
fn from(err: anyhow::Error) -> Self {
|
||||||
|
ValidateFormError::InternalServerError(AppError::from(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<AppError> for ValidateFormError<T> {
|
||||||
|
fn from(err: AppError) -> Self {
|
||||||
|
ValidateFormError::InternalServerError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in new issue