More egonomic form handling

main
Wes Holland 9 months ago
parent bef1ebdae7
commit 453d36dd86

@ -4,7 +4,6 @@ use crate::db::inventory_item::does_inventory_item_allow_fractional_units;
use crate::error::AppError;
use crate::session::SessionUser;
use crate::util::currency::dollars_string_to_int_cents;
use crate::util::extract::htmx_form_data::{HtmxFormData, HtmxFormDataError};
use askama::Template;
use askama_axum::{IntoResponse, Response};
use axum::extract::{Path, State};
@ -14,13 +13,17 @@ use serde::Deserialize;
use sqlx::SqlitePool;
use tracing::info;
use crate::app::routes::AppState;
use crate::util::form::extractors::{FormWithPathVars, ValidForm};
use crate::util::form::field_error::FieldError;
use crate::util::form::field_value::{FloatField, StringField};
use crate::util::form::validate_form::{ValidateForm, ValidateFormError};
#[derive(Template)]
#[derive(Template, Debug, Default)]
#[template(path = "item/adjustment/positive-adjustment-form.html")]
pub struct PositiveAdjustmentFormTemplate {
pub item_id: i64,
pub amount_error: &'static str,
pub price_error: &'static str,
pub amount: StringField,
pub price: StringField,
}
#[derive(Deserialize, Debug)]
@ -29,41 +32,43 @@ pub struct PositiveAdjustmentFormData {
pub price: String,
}
#[debug_handler]
#[async_trait]
impl ValidateForm for FormWithPathVars<i64, PositiveAdjustmentFormData> {
type ValidationErrorResponse = PositiveAdjustmentFormTemplate;
async fn validate(self, state: &AppState) -> Result<Self, ValidateFormError<Self::ValidationErrorResponse>> {
let item_id = self.path_data;
let fractional_units_allowed = does_inventory_item_allow_fractional_units(&state.db, item_id).await?;
let amount = FloatField::new(self.form_data.amount)
.invalid_if(|v| *v == 0.0 || v.is_nan() || v.is_sign_negative(), FieldError::PositiveNumber)
.invalid_if(|v| !(fractional_units_allowed || v.fract() == 0.0), FieldError::WholeNumber)
.map(|v| if fractional_units_allowed { format!("{:.2}", v) } else { format!("{:.0}", v) });
let price = StringField::new(self.form_data.price.clone())
.invalid_if(|v| dollars_string_to_int_cents(v).is_err(), FieldError::Currency);
if amount.is_error() || price.is_error() {
return Err(ValidateFormError::ValidationError(Self::ValidationErrorResponse {
item_id,
amount,
price,
}));
}
Ok(self)
}
}
pub async fn positive_adjustment_form_post(
State(db): State<SqlitePool>,
Path(id): Path<i64>,
user: SessionUser,
form_data: Form<PositiveAdjustmentFormData>,
data: ValidForm<FormWithPathVars<i64,PositiveAdjustmentFormData>>
) -> Result<Response, AppError> {
let fractional_units_allowed = does_inventory_item_allow_fractional_units(&db, id).await?;
let amount_error = if form_data.amount == 0.0 {
"Please input a non-zero amount"
} else if !(fractional_units_allowed || form_data.amount.fract() == 0.0) {
"Please input a whole number"
} else {
""
};
let price = dollars_string_to_int_cents(&form_data.price);
let price_error = if price.is_err() {
"Please input a dollar amount"
} else {
""
};
if !(amount_error.is_empty() && price_error.is_empty()) {
return Ok(PositiveAdjustmentFormTemplate {
item_id: id,
amount_error,
price_error,
}
.into_response());
}
let price = price?;
let unit_price = (price as f64 / form_data.amount) as i64;
let id = data.path_data;
let price = dollars_string_to_int_cents(&data.form_data.price)?;
let unit_price = (price as f64 / data.form_data.amount) as i64;
let trigger_events = vec![
HxEvent::from("form-submit-success"),
@ -72,11 +77,11 @@ pub async fn positive_adjustment_form_post(
let timestamp = chrono::Utc::now();
let adjustment_amount = form_data.amount;
let adjustment_amount = data.form_data.amount;
info!(
"Add adjustment form: {:?} for user {}",
form_data, user.name
data.form_data, user.name
);
let _new_id = add_adjustment(
@ -95,19 +100,17 @@ pub async fn positive_adjustment_form_post(
HxResponseTrigger::normal(trigger_events),
PositiveAdjustmentFormTemplate {
item_id: id,
amount_error: "",
price_error: "",
..Default::default()
}
.into_response(),
)
.into_response())
.into_response())
}
pub async fn positive_adjustment_form_get(Path(id): Path<i64>) -> Result<Response, AppError> {
Ok(PositiveAdjustmentFormTemplate {
item_id: id,
amount_error: "",
price_error: "",
..Default::default()
}
.into_response())
}

@ -3,45 +3,47 @@ use crate::session::SessionUser;
use askama::Template;
use askama_axum::{IntoResponse, Response};
use axum::extract::State;
use axum::{async_trait, debug_handler, Form};
use axum::{async_trait, debug_handler};
use axum_htmx::{HxEvent, HxResponseTrigger};
use serde::Deserialize;
use tracing::info;
use crate::app::routes::AppState;
use crate::db::display_unit::DbDisplayUnit;
use crate::db;
use crate::util::extract::htmx_form_data::{HtmxFormData, HtmxFormDataError};
use crate::util::extract::validated_form::ValidatedForm;
use crate::util::extract::form_helpers::deserialize_form_checkbox;
#[derive(Template, Debug)]
use crate::util::form::field_error::FieldError;
use crate::util::form::field_value::{BoolField, FloatField, StringField};
use crate::util::form::validate_form::{ValidateForm, ValidateFormError};
use crate::util::form::helpers::deserialize_form_checkbox;
use crate::util::form::base_response::BaseResponse;
use crate::util::form::extractors::{FormBase, ValidForm};
#[derive(Template, Debug, Default)]
#[template(path = "item/item-create-form.html")]
pub struct CreateItemFormTemplate {
pub display_units: Vec<DbDisplayUnit>,
pub name_value: String,
pub name_error: &'static str,
pub display_unit_value: String,
pub display_unit_error: &'static str,
pub reorder_point_value: String,
pub reorder_point_error: &'static str,
pub pims_id_value: String,
pub pims_id_error: &'static str,
pub vetcove_id_value: String,
pub vetcove_id_error: &'static str,
pub allow_fractional_units_value: bool,
pub name: StringField,
pub display_unit_value: StringField,
pub reorder_point: StringField,
pub pims_id: StringField,
pub vetcove_id: StringField,
pub allow_fractional_units: BoolField,
}
impl CreateItemFormTemplate {
pub fn clear_inputs(&mut self) {
self.name_value.clear();
self.display_unit_value.clear();
self.reorder_point_value.clear();
self.pims_id_value.clear();
self.vetcove_id_value.clear();
self.allow_fractional_units_value = false;
#[async_trait]
impl BaseResponse for CreateItemFormTemplate {
type ResponseType = Self;
async fn base_response(state: &AppState) -> Result<Self::ResponseType, AppError> {
let db = &state.db;
let display_units = db::display_unit::get_display_units(&db).await?;
Ok(Self {
display_units,
..Default::default()
})
}
}
#[derive(Deserialize, Debug)]
pub struct CreateItemFormData {
name: String,
@ -54,122 +56,72 @@ pub struct CreateItemFormData {
}
#[async_trait]
impl HtmxFormData for CreateItemFormData {
type FormTemplate = CreateItemFormTemplate;
async fn validate(self, state: &AppState) -> Result<Self, HtmxFormDataError<Self::FormTemplate>> {
let mut base = Self::base_template(&state).await?;
let display_units = &base.display_units;
let name_error = if self.name.is_empty() {
"Please provide a name"
} else {
""
};
let display_unit_error = if self.display_unit.is_empty() {
"Please provide a display unit"
} else if !display_units.iter().any(|x| x.abbreviation.eq(&self.display_unit)){
"Invalid display unit"
} else {
""
};
let reorder_point_error = if self.reorder_point.is_nan()
|| self.reorder_point.is_infinite()
|| self.reorder_point.is_sign_negative()
{
"Provide a positive number"
} else if !(self.allow_fractional_units || self.reorder_point.fract() == 0.0) {
"Fractional units not allowed"
} else {
""
};
let pims_id_error = if let Some(pims_id) = &self.pims_id {
if pims_id.chars().any(char::is_whitespace) {
"Invalid PIMS id"
}
else {
""
}
}
else {
""
};
let vetcove_id_error = if let Some(vetcove_id) = &self.vetcove_id {
if !vetcove_id.chars().all(|c| c.is_ascii_digit()) {
"Invalid Vectcove id"
}
else {
""
}
}
else {
""
};
if !(name_error.is_empty()
&& display_unit_error.is_empty()
&& reorder_point_error.is_empty()
&& pims_id_error.is_empty()
&& vetcove_id_error.is_empty()) {
base.name_value = self.name;
base.name_error = name_error;
base.display_unit_value = self.display_unit;
base.display_unit_error = display_unit_error;
base.reorder_point_value = format!("{:.2}", self.reorder_point);
base.reorder_point_error = reorder_point_error;
base.pims_id_value = self.pims_id.as_deref().unwrap_or_default().to_string();
base.pims_id_error = pims_id_error;
base.vetcove_id_value = self.vetcove_id.as_deref().unwrap_or_default().to_string();
base.vetcove_id_error = vetcove_id_error;
base.allow_fractional_units_value = self.allow_fractional_units;
return Err(HtmxFormDataError::ValidationError(base));
impl ValidateForm for FormBase<CreateItemFormData> {
type ValidationErrorResponse = CreateItemFormTemplate;
async fn validate(self, state: &AppState) -> Result<Self, ValidateFormError<Self::ValidationErrorResponse>> {
let display_units = db::display_unit::get_display_units(&state.db).await?;
let name = StringField::new(self.form_data.name.clone())
.invalid_if(|v| v.is_empty(), FieldError::Required);
let display_unit_value = StringField::new(self.form_data.display_unit.clone())
.invalid_if(|v| v.is_empty(), FieldError::Required)
.invalid_if(|v| !display_units.iter().any(
|x| x.abbreviation.eq(&self.form_data.display_unit)),
FieldError::SelectOption)
.clear_value_if_error();
let reorder_point = FloatField::new(self.form_data.reorder_point.clone())
.invalid_if(|v| v.is_nan() || v.is_infinite() || v.is_sign_negative(), FieldError::PositiveNumber)
.invalid_if(|v| !(self.form_data.allow_fractional_units || v.fract() == 0.0), FieldError::WholeNumber)
.map(|v| format!("{:.2}", v));
let pims_id = StringField::new(self.form_data.pims_id.clone().unwrap_or_default())
.invalid_if(|v| v.chars().any(char::is_whitespace), FieldError::ValidIdentifier);
let vetcove_id = StringField::new(self.form_data.vetcove_id.clone().unwrap_or_default())
.invalid_if(|v| !v.chars().all(|c| c.is_ascii_digit()), FieldError::ValidIdentifier);
let allow_fractional_units = BoolField::new(self.form_data.allow_fractional_units);
if name.is_error() ||
display_unit_value.is_error() ||
reorder_point.is_error() ||
pims_id.is_error() ||
vetcove_id.is_error() {
return Err(ValidateFormError::ValidationError(
Self::ValidationErrorResponse {
display_units,
name,
display_unit_value,
reorder_point,
pims_id,
vetcove_id,
allow_fractional_units,
}));
}
Ok(self)
}
async fn base_template(state: &AppState) -> anyhow::Result<Self::FormTemplate> {
let db = &state.db;
let display_units = db::display_unit::get_display_units(&db).await?;
Ok(Self::FormTemplate {
display_units,
name_value: "".to_owned(),
name_error: "",
display_unit_value: "".to_owned(),
display_unit_error: "",
reorder_point_value: "".to_owned(),
reorder_point_error: "",
pims_id_value: "".to_owned(),
pims_id_error: "",
vetcove_id_value: "".to_owned(),
vetcove_id_error: "",
allow_fractional_units_value: false,
})
}
}
#[debug_handler]
pub async fn create_item_form_post(
State(state): State<AppState>,
user: SessionUser,
form_data: ValidatedForm<CreateItemFormData>,
form: ValidForm<FormBase<CreateItemFormData>>,
) -> Result<Response, AppError> {
let form_data = &form.form_data;
let _new_id = db::inventory_item::add_inventory_item(&state.db, &form_data.name, form_data.reorder_point,
form_data.allow_fractional_units, &form_data.display_unit,
&form_data.pims_id, &form_data.vetcove_id,
).await?;
let fresh_form = CreateItemFormData::base_template(&state).await?;
let fresh_form = CreateItemFormTemplate::base_response(&state).await?;
let events = vec![
HxEvent::from("form-submit-success"),
@ -186,5 +138,5 @@ pub async fn create_item_form_post(
pub async fn create_item_form_get(
State(state): State<AppState>,
) -> Result<Response, AppError> {
Ok(CreateItemFormData::base_template(&state).await?.into_response())
Ok(CreateItemFormTemplate::base_response(&state).await?.into_response())
}

@ -24,7 +24,7 @@ pub async fn init() -> Result<(SessionManagerLayer<SqliteStore>, JoinHandle<Resu
let session_store = SqliteStore::new(session_db);
session_store.migrate().await?;
// This guy forms the session cookies
// This guy form the session cookies
// The session manager layer is the glue between the session store
// and the handlers. The options basically define the options of
// the cookies given to the client

@ -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)
}
}

@ -2,4 +2,4 @@ pub mod time;
pub mod currency;
pub mod parsing;
pub mod formatting;
pub mod extract;
pub mod form;

@ -13,17 +13,18 @@
step="0.01"
placeholder="Amount"
aria-label="amount"
value="{{ amount.value }}"
required
{% if !amount_error.is_empty() -%}
{% if amount.is_error() -%}
aria-invalid="true"
aria-describedby="invalid-amount"
{% endif -%}
/>
{% if !amount_error.is_empty() -%}
{% if amount.is_error() -%}
<small id="invalid-amount"
class="mt-2 text-sm text-cerise"
>
{{ amount_error }}
{{ amount.error.unwrap() }}
</small>
{% endif -%}
</div>
@ -36,17 +37,18 @@
class="block max-w-56 rounded-lg border border-neutral-900 p-2.5 text-sm text-neutral-900 focus:border-paynes-gray focus:ring-paynes-gray dark:text-slate-100"
placeholder="Price"
aria-label="price"
value="{{ price.value }}"
required
{% if !price_error.is_empty() -%}
{% if price.is_error() -%}
aria-invalid="true"
aria-describedby="invalid-price"
{% endif -%}
/>
{% if !price_error.is_empty() -%}
{% if price.is_error() -%}
<small id="invalid-price"
class="mt-2 text-sm text-cerise"
>
{{ price_error }}
{{ price.error.unwrap() }}
</small>
{% endif -%}
</div>

@ -15,16 +15,16 @@
name="name"
class="block w-11/12 p-2 rounded-lg border border-neutral-900 text-sm text-neutral-900 focus:border-paynes-gray focus:ring-paynes-gray dark:text-slate-100"
aria-label="name"
value="{{ name_value }}"
value="{{ name.value }}"
required
{% if !name_error.is_empty() -%}
{% if name.is_error() -%}
aria-invalid="true"
aria-describedby="invalid-name"
{% endif -%}
/>
{% if !name_error.is_empty() -%}
{% if name.is_error() -%}
<small id="invalid-name" class="block mt-2 text-sm text-cerise">
{{ name_error }}
{{ name.error.unwrap() }}
</small>
{% endif -%}
</div>
@ -38,15 +38,15 @@
step="0.01"
class="block w-3/4 p-2 rounded-lg border border-neutral-900 text-sm text-neutral-900 focus:border-paynes-gray focus:ring-paynes-gray dark:text-slate-100"
aria-label="reorder point"
value="{{ reorder_point_value }}"
{% if !reorder_point_error.is_empty() -%}
value="{{ reorder_point.value }}"
{% if reorder_point.is_error() -%}
aria-invalid="true"
aria-describedby="invalid-reorder-point"
{% endif -%}
/>
{% if !reorder_point_error.is_empty() -%}
{% if reorder_point.is_error() -%}
<small id="invalid-reorder-point" class="block mt-2 text-sm text-cerise">
{{ pims_id_error }}
{{ reorder_point.error.unwrap() }}
</small>
{% endif -%}
</div>
@ -59,18 +59,18 @@
class="block rounded-lg border border-neutral-900 p-2 text-sm text-neutral-900 focus:border-paynes-gray focus:ring-paynes-gray dark:text-slate-100"
aria-label="name"
required
{% if !display_unit_error.is_empty() -%}
{% if display_unit_value.is_error() -%}
aria-invalid="true"
aria-describedby="invalid-display-unit"
{% endif -%}
>
{% for unit in display_units -%}
<option value="{{ unit.abbreviation }}" {% if unit.abbreviation == display_unit_value %} selected {% endif %}>{{ unit.unit }}</option>
<option value="{{ unit.abbreviation }}" {% if unit.abbreviation == display_unit_value.value %} selected {% endif %}>{{ unit.unit }}</option>
{% endfor %}
</select>
{% if !display_unit_error.is_empty() -%}
{% if display_unit_value.is_error() -%}
<small id="invalid-display-unit" class="block mt-2 text-sm text-cerise">
{{ display_unit_error }}
{{ display_unit_value.error.unwrap() }}
</small>
{% endif -%}
</div>
@ -82,7 +82,7 @@
id="allow_fractional_units"
name="allow_fractional_units"
class="block w-4 h-4 text-dark-cyan border-slate-100 rounded-sm focus:ring-space-cadet focus:ring-2"
{% if allow_fractional_units_value -%} checked {% endif -%}
{% if allow_fractional_units.value -%} checked {% endif -%}
/>
</div>
@ -94,16 +94,16 @@
id="pims_id"
name="pims_id"
class="block p-2 rounded-lg border border-neutral-900 text-sm text-neutral-900 focus:border-paynes-gray focus:ring-paynes-gray dark:text-slate-100"
value="{{ pims_id_value }}"
value="{{ pims_id.value }}"
aria-label="pims id"
{% if !pims_id_error.is_empty() -%}
{% if pims_id.is_error() -%}
aria-invalid="true"
aria-describedby="invalid-pims-id"
{% endif -%}
/>
{% if !pims_id_error.is_empty() -%}
{% if pims_id.is_error() -%}
<small id="invalid-pims-id" class="block mt-2 text-sm text-cerise">
{{ pims_id_error }}
{{ pims_id.error.unwrap() }}
</small>
{% endif -%}
</div>
@ -115,16 +115,16 @@
id="vetcove_id"
name="vetcove_id"
class="block p-2 rounded-lg border border-neutral-900 text-sm text-neutral-900 focus:border-paynes-gray focus:ring-paynes-gray dark:text-slate-100"
value="{{ vetcove_id_value }}"
value="{{ vetcove_id.value }}"
aria-label="vetcove id"
{% if !vetcove_id_error.is_empty() -%}
{% if vetcove_id.is_error() -%}
aria-invalid="true"
aria-describedby="invalid-vetcove-id"
{% endif -%}
/>
{% if !vetcove_id_error.is_empty() -%}
{% if vetcove_id.is_error() -%}
<small id="invalid-vetcove-id" class="block mt-2 text-sm text-cerise">
{{ vetcove_id_error }}
{{ vetcove_id.error.unwrap() }}
</small>
{% endif -%}
</div>

Loading…
Cancel
Save

Powered by TurnKey Linux.