Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions example/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct CreateUser {
url: String,

#[validate(custom(function = validate_one_locale_required, error = OneLocaleRequiredError))]
#[validate(length(min = 1))]
locales: Vec<String>,
}

Expand Down
79 changes: 68 additions & 11 deletions packages/fortifier-macros/src/validate/field.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
use convert_case::{Case, Casing};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Field, Result};
use quote::{ToTokens, format_ident, quote};
use syn::{Field, Ident, Result};

use crate::{
validation::Validation,
validations::{Custom, Email, Length, Url},
};

pub struct ValidateField {
error_type_ident: Ident,
expr: TokenStream,
validations: Vec<Box<dyn Validation>>,
}

impl ValidateField {
pub fn parse(expr: TokenStream, field: &Field) -> Result<Self> {
pub fn parse(
type_prefix: &Ident,
ident: Ident,
expr: TokenStream,
field: &Field,
) -> Result<Self> {
let error_ident = format_ident!("{}", ident.to_string().to_case(Case::UpperCamel));
let error_type_ident = format_ident!("{type_prefix}{error_ident}ValidationError");

let mut result = Self {
error_type_ident,
expr,
validations: vec![],
};
Expand Down Expand Up @@ -48,28 +59,74 @@ impl ValidateField {
Ok(result)
}

pub fn error_type(&self) -> TokenStream {
// TODO: Merge error types
pub fn error_type(
&self,
ident: &Ident,
field_error_ident: &Ident,
) -> (TokenStream, Option<TokenStream>) {
if self.validations.len() > 1 {
let ident = format_ident!("{}{}ValidationError", ident, field_error_ident);
let variant_ident = self.validations.iter().map(|validation| validation.ident());
let variant_type = self
.validations
.iter()
.map(|validation| validation.error_type());

self.validations
.first()
.map(|validation| validation.error_type())
.unwrap_or_else(|| quote!(()))
(
ident.to_token_stream(),
Some(quote! {
#[derive(Debug)]
enum #ident {
#( #variant_ident(#variant_type) ),*
}
}),
)
} else if let Some(validation) = self.validations.first() {
(validation.error_type(), None)
} else {
(quote!(()), None)
}
}

pub fn sync_validations(&self) -> Vec<TokenStream> {
let error_type_ident = &self.error_type_ident;

self.validations
.iter()
.filter(|validation| !validation.is_async())
.map(|validation| validation.tokens(&self.expr))
.map(|validation| {
let validation_ident = validation.ident();
let tokens = validation.tokens(&self.expr);

if self.validations.len() > 1 {
quote! {
#tokens.map_err(#error_type_ident::#validation_ident)
}
} else {
tokens
}
})
.collect()
}

pub fn async_validations(&self) -> Vec<TokenStream> {
let error_type_ident = &self.error_type_ident;

self.validations
.iter()
.filter(|validation| validation.is_async())
.map(|validation| validation.tokens(&self.expr))
.map(|validation| {
let validation_ident = validation.ident();
let tokens = validation.tokens(&self.expr);

if self.validations.len() > 1 {
quote! {
#tokens.map_err(#error_type_ident::#validation_ident)
}
} else {
tokens
}
})
.collect()
}
}
44 changes: 38 additions & 6 deletions packages/fortifier-macros/src/validate/struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ impl ValidateNamedStruct {

let expr = quote!(self.#field_ident);

result
.fields
.insert(field_ident.clone(), ValidateField::parse(expr, field)?);
result.fields.insert(
field_ident.clone(),
ValidateField::parse(&input.ident, field_ident.clone(), expr, field)?,
);
}

Ok(result)
Expand All @@ -71,15 +72,21 @@ impl ToTokens for ValidateNamedStruct {
let error_ident = &self.error_ident;
let mut error_field_idents = vec![];
let mut error_field_types = vec![];
let mut error_field_enums = vec![];
let mut sync_validations = vec![];
let mut async_validations = vec![];

for (field_ident, field) in &self.fields {
let field_error_ident =
format_ident!("{}", &field_ident.to_string().to_case(Case::UpperCamel));

let (error_type, error_enum) = field.error_type(ident, &field_error_ident);

error_field_idents.push(field_error_ident.clone());
error_field_types.push(field.error_type());
error_field_types.push(error_type);
if let Some(error_enum) = error_enum {
error_field_enums.push(error_enum);
}

for validation in field.sync_validations() {
sync_validations.push(quote! {
Expand All @@ -101,19 +108,25 @@ impl ToTokens for ValidateNamedStruct {
tokens.append_all(quote! {
use fortifier::*;

#[allow(dead_code)]
#[derive(Debug)]
enum #error_ident {
#( #error_field_idents(#error_field_types) ),*
}

#[automatically_derived]
impl ::std::fmt::Display for #error_ident {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
write!(f, "{self:#?}")
}
}

#[automatically_derived]
impl ::std::error::Error for #error_ident {}

#(#error_field_enums)*

#[automatically_derived]
impl Validate for #ident {
type Error = #error_ident;

Expand Down Expand Up @@ -163,9 +176,15 @@ impl ValidateUnnamedStruct {

for (index, field) in fields.unnamed.iter().enumerate() {
let index = Literal::usize_unsuffixed(index);
let field_ident = format_ident!("F{index}");
let expr = quote!(self.#index);

result.fields.push(ValidateField::parse(expr, field)?);
result.fields.push(ValidateField::parse(
&input.ident,
field_ident,
expr,
field,
)?);
}

Ok(result)
Expand All @@ -178,14 +197,20 @@ impl ToTokens for ValidateUnnamedStruct {
let error_ident = &self.error_ident;
let mut error_field_idents = vec![];
let mut error_field_types = vec![];
let mut error_field_enums = vec![];
let mut sync_validations = vec![];
let mut async_validations = vec![];

for (index, field) in self.fields.iter().enumerate() {
let field_error_ident = format_ident!("F{index}");

let (error_type, error_enum) = field.error_type(ident, &field_error_ident);

error_field_idents.push(field_error_ident.clone());
error_field_types.push(field.error_type());
error_field_types.push(error_type);
if let Some(error_enum) = error_enum {
error_field_enums.push(error_enum);
}

for validation in field.sync_validations() {
sync_validations.push(quote! {
Expand All @@ -207,19 +232,25 @@ impl ToTokens for ValidateUnnamedStruct {
tokens.append_all(quote! {
use fortifier::*;

#[allow(dead_code)]
#[derive(Debug)]
enum #error_ident {
#( #error_field_idents(#error_field_types) ),*
}

#[automatically_derived]
impl ::std::fmt::Display for #error_ident {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
write!(f, "{self:#?}")
}
}

#[automatically_derived]
impl ::std::error::Error for #error_ident {}

#(#error_field_enums)*

#[automatically_derived]
impl Validate for #ident {
type Error = #error_ident;

Expand Down Expand Up @@ -272,6 +303,7 @@ impl ToTokens for ValidateUnitStruct {
tokens.append_all(quote! {
use fortifier::ValidationErrors;

#[automatically_derived]
impl Validate for #ident {
type Error = ::std::convert::Infallible;

Expand Down
4 changes: 3 additions & 1 deletion packages/fortifier-macros/src/validation.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use proc_macro2::TokenStream;
use syn::{Result, meta::ParseNestedMeta};
use syn::{Ident, Result, meta::ParseNestedMeta};

pub trait Validation {
fn parse(_meta: &ParseNestedMeta<'_>) -> Result<Self>
Expand All @@ -8,6 +8,8 @@ pub trait Validation {

fn is_async(&self) -> bool;

fn ident(&self) -> Ident;

fn error_type(&self) -> TokenStream;

fn tokens(&self, expr: &TokenStream) -> TokenStream;
Expand Down
9 changes: 7 additions & 2 deletions packages/fortifier-macros/src/validations/custom.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::{LitBool, Path, Result, Type, meta::ParseNestedMeta};
use quote::{ToTokens, format_ident, quote};
use syn::{Ident, LitBool, Path, Result, Type, meta::ParseNestedMeta};

use crate::validation::Validation;

Expand Down Expand Up @@ -57,6 +57,11 @@ impl Validation for Custom {
self.is_async
}

fn ident(&self) -> Ident {
// TODO: Determine ident from function or error type.
format_ident!("Custom")
}

fn error_type(&self) -> TokenStream {
self.error_type.to_token_stream()
}
Expand Down
8 changes: 6 additions & 2 deletions packages/fortifier-macros/src/validations/email.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Result, meta::ParseNestedMeta};
use quote::{format_ident, quote};
use syn::{Ident, Result, meta::ParseNestedMeta};

use crate::validation::Validation;

Expand All @@ -16,6 +16,10 @@ impl Validation for Email {
false
}

fn ident(&self) -> Ident {
format_ident!("Email")
}

fn error_type(&self) -> TokenStream {
quote!(EmailError)
}
Expand Down
8 changes: 6 additions & 2 deletions packages/fortifier-macros/src/validations/length.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Expr, Result, meta::ParseNestedMeta};
use quote::{format_ident, quote};
use syn::{Expr, Ident, Result, meta::ParseNestedMeta};

use crate::validation::Validation;

Expand Down Expand Up @@ -43,6 +43,10 @@ impl Validation for Length {
false
}

fn ident(&self) -> Ident {
format_ident!("Length")
}

fn error_type(&self) -> TokenStream {
quote!(LengthError<usize>)
}
Expand Down
8 changes: 6 additions & 2 deletions packages/fortifier-macros/src/validations/url.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Result, meta::ParseNestedMeta};
use quote::{format_ident, quote};
use syn::{Ident, Result, meta::ParseNestedMeta};

use crate::validation::Validation;

Expand All @@ -16,6 +16,10 @@ impl Validation for Url {
false
}

fn ident(&self) -> Ident {
format_ident!("Url")
}

fn error_type(&self) -> TokenStream {
quote!(UrlError)
}
Expand Down