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};
|
||||
|
||||
/**
|
||||
Deserialize a checkbox field from a html form into a bool
|
||||
*/
|
||||
pub fn deserialize_form_checkbox<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
||||
where D: Deserializer<'de> {
|
||||
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