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
4 changes: 2 additions & 2 deletions packages/fortifier-macros/src/validate/enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use syn::{DataEnum, DeriveInput, Ident, Result, Variant, Visibility};
use crate::{
validate::{
attributes::enum_attributes,
field::{LiteralOrIdent, ValidateFieldPrefix},
field::{LiteralOrIdent, ValidateFieldPrefix, format_error_ident},
fields::ValidateFields,
},
validation::Execution,
Expand All @@ -23,7 +23,7 @@ impl<'a> ValidateEnum<'a> {
let mut result = ValidateEnum {
visibility: &input.vis,
ident: &input.ident,
error_ident: format_ident!("{}ValidationError", input.ident),
error_ident: format_error_ident(&input.ident),
variants: Vec::with_capacity(data.variants.len()),
};

Expand Down
43 changes: 34 additions & 9 deletions packages/fortifier-macros/src/validate/field.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use convert_case::{Case, Casing};
use proc_macro2::{Literal, TokenStream};
use quote::{ToTokens, format_ident, quote};
use syn::{Field, Ident, Result, Visibility};
use syn::{Error, Field, Ident, Result, Visibility};

use crate::{
validate::{attributes::enum_attributes, r#type::should_validate_type},
validate::{
attributes::enum_attributes,
r#type::{KnownOrUnknown, should_validate_type},
},
validation::{Execution, Validation},
validations::{Custom, EmailAddress, Length, Nested, PhoneNumber, Regex, Url},
};
Expand Down Expand Up @@ -49,7 +52,7 @@ impl<'a> ValidateField<'a> {
LiteralOrIdent::Literal(literal) => format_ident!("F{literal}"),
LiteralOrIdent::Ident(ident) => upper_camel_ident(ident),
};
let error_type_ident = format_ident!("{type_prefix}{error_ident}ValidationError");
let error_type_ident = format_error_ident_with_prefix(type_prefix, &error_ident);

let mut result = Self {
visibility,
Expand All @@ -58,7 +61,7 @@ impl<'a> ValidateField<'a> {
error_type_ident,
validations: vec![],
};
let mut skip = false;
let mut skip_nested = false;

for attr in &field.attrs {
if attr.path().is_ident("validate") {
Expand All @@ -76,6 +79,11 @@ impl<'a> ValidateField<'a> {
} else if meta.path.is_ident("length") {
result.validations.push(Box::new(Length::parse(&meta)?));

Ok(())
} else if meta.path.is_ident("nested") {
result.validations.push(Box::new(Nested::parse(&meta)?));
skip_nested = true;

Ok(())
} else if meta.path.is_ident("phone_number") {
result
Expand All @@ -92,7 +100,7 @@ impl<'a> ValidateField<'a> {

Ok(())
} else if meta.path.is_ident("skip") {
skip = true;
skip_nested = true;

Ok(())
} else {
Expand All @@ -104,9 +112,18 @@ impl<'a> ValidateField<'a> {

// TODO: Use enum/struct generics to determine if a generic field type supports nested validation.
// TODO: Remove the validations empty check after resolving the issue above.
if !skip && result.validations.is_empty() && should_validate_type(&field.ty) {
// TODO: Nested validation
result.validations.push(Box::new(Nested::new()));
if !skip_nested
&& result.validations.is_empty()
&& let Some(nested_type) = should_validate_type(&field.ty)
{
if let KnownOrUnknown::Known(nested_type) = nested_type {
result.validations.push(Box::new(Nested::new(nested_type)));
} else {
return Err(Error::new_spanned(
field,
"error type must be specified using `#[validate(nested(error_type = MyErrorType))]`",
));
}
}

Ok(result)
Expand All @@ -124,7 +141,7 @@ impl<'a> ValidateField<'a> {
if self.validations.len() > 1 {
let attributes = enum_attributes();
let visibility = &self.visibility;
let ident = format_ident!("{}{}ValidationError", ident, self.error_ident);
let ident = format_error_ident_with_prefix(ident, &self.error_ident);
let variant_ident = self.validations.iter().map(|validation| validation.ident());
let variant_type = self
.validations
Expand Down Expand Up @@ -194,3 +211,11 @@ fn upper_camel_ident(ident: &Ident) -> Ident {
format_ident!("{}", s.to_case(Case::UpperCamel))
}
}

pub fn format_error_ident(ident: &Ident) -> Ident {
format_ident!("{}ValidationError", ident)
}

pub fn format_error_ident_with_prefix(prefix: &Ident, ident: &Ident) -> Ident {
format_ident!("{}{}ValidationError", prefix, ident)
}
8 changes: 4 additions & 4 deletions packages/fortifier-macros/src/validate/fields.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use proc_macro2::{Literal, TokenStream};
use quote::{ToTokens, format_ident, quote};
use quote::{ToTokens, quote};
use syn::{Fields, FieldsNamed, FieldsUnnamed, Ident, Result, Visibility};

use crate::{
validate::{
attributes::enum_attributes,
field::{LiteralOrIdent, ValidateField, ValidateFieldPrefix},
field::{LiteralOrIdent, ValidateField, ValidateFieldPrefix, format_error_ident},
},
validation::Execution,
};
Expand Down Expand Up @@ -64,7 +64,7 @@ pub struct ValidateNamedFields<'a> {

impl<'a> ValidateNamedFields<'a> {
fn parse(visibility: &'a Visibility, ident: Ident, fields: &'a FieldsNamed) -> Result<Self> {
let error_ident = format_ident!("{}ValidationError", ident);
let error_ident = format_error_ident(&ident);

let mut result = Self {
visibility,
Expand Down Expand Up @@ -127,7 +127,7 @@ pub struct ValidateUnnamedFields<'a> {

impl<'a> ValidateUnnamedFields<'a> {
fn parse(visibility: &'a Visibility, ident: Ident, fields: &'a FieldsUnnamed) -> Result<Self> {
let error_ident = format_ident!("{}ValidationError", ident);
let error_ident = format_error_ident(&ident);

let mut result = Self {
visibility,
Expand Down
86 changes: 61 additions & 25 deletions packages/fortifier-macros/src/validate/type.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::{GenericArgument, Path, PathArguments, Type, TypeParamBound};

use crate::validate::field::format_error_ident;

const PRIMITIVE_AND_BUILT_IN_TYPES: [&str; 18] = [
"bool", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize",
"f32", "f64", "char", "str", "String",
];

const CONTAINER_TYPES: [&str; 20] = [
const INDEXED_CONTAINER_TYPES: [&str; 16] = [
"Arc",
"BTreeMap",
"BTreeSet",
"HashMap",
"HashSet",
"LinkedList",
"Option",
"Rc",
"Vec",
"VecDeque",
"std::collections::BTreeMap",
"std::collections::BTreeSet",
"std::collections::HashMap",
"std::collections::HashSet",
"std::collections::LinkedList",
"std::collections::VecDeque",
Expand All @@ -28,6 +28,13 @@ const CONTAINER_TYPES: [&str; 20] = [
"std::vec::Vec",
];

const KEYED_CONTAINER_TYPES: [&str; 4] = [
"BTreeMap",
"HashMap",
"std::collections::BTreeMap",
"std::collections::HashMap",
];

fn path_to_string(path: &Path) -> String {
// TODO: This is probably slow, replace with comparisons.
path.segments
Expand All @@ -42,59 +49,88 @@ fn is_validate_path(path: &Path) -> bool {
path_string == "Validate" || path_string == "fortifier::Validate"
}

fn should_validate_generic_argument(arg: &GenericArgument) -> bool {
fn should_validate_generic_argument(arg: &GenericArgument) -> Option<KnownOrUnknown<TokenStream>> {
match arg {
GenericArgument::Lifetime(_) => true,
GenericArgument::Lifetime(_) => Some(KnownOrUnknown::Unknown),
GenericArgument::Type(r#type) => should_validate_type(r#type),
GenericArgument::Const(_expr) => todo!(),
GenericArgument::AssocType(_assoc_type) => todo!(),
GenericArgument::AssocConst(_assoc_const) => todo!(),
GenericArgument::Constraint(_constraint) => todo!(),
_ => true,
_ => Some(KnownOrUnknown::Unknown),
}
}

fn should_validate_path(path: &Path) -> bool {
fn should_validate_path(path: &Path) -> Option<KnownOrUnknown<TokenStream>> {
if let Some(ident) = path.get_ident() {
return !PRIMITIVE_AND_BUILT_IN_TYPES.contains(&ident.to_string().as_str());
return if PRIMITIVE_AND_BUILT_IN_TYPES.contains(&ident.to_string().as_str()) {
None
} else {
Some(KnownOrUnknown::Known(
format_error_ident(ident).to_token_stream(),
))
};
}
let path_string = path_to_string(path);
let path_string = path_string.as_str();

if CONTAINER_TYPES.contains(&path_string.as_str())
if INDEXED_CONTAINER_TYPES.contains(&path_string)
&& let Some(segment) = path.segments.last()
&& let PathArguments::AngleBracketed(arguments) = &segment.arguments
&& !arguments.args.iter().all(should_validate_generic_argument)
&& let Some(argument) = arguments.args.first()
{
return false;
return should_validate_generic_argument(argument).map(|error_type| match error_type {
KnownOrUnknown::Known(error_type) => KnownOrUnknown::Known(
quote!(::fortifier::ValidationErrors<::fortifier::IndexedValidationError<#error_type>>)
),
KnownOrUnknown::Unknown => KnownOrUnknown::Unknown
});
}

true
// TODO: Determine error type.
if KEYED_CONTAINER_TYPES.contains(&path_string)
&& let Some(segment) = path.segments.last()
&& let PathArguments::AngleBracketed(arguments) = &segment.arguments
&& !arguments
.args
.iter()
.all(|arg| should_validate_generic_argument(arg).is_some())
{
return None;
}

Some(KnownOrUnknown::Unknown)
}

pub enum KnownOrUnknown<T> {
Known(T),
Unknown,
}

pub fn should_validate_type(r#type: &Type) -> bool {
pub fn should_validate_type(r#type: &Type) -> Option<KnownOrUnknown<TokenStream>> {
match r#type {
Type::Array(r#type) => should_validate_type(&r#type.elem),
Type::BareFn(_) => false,
Type::BareFn(_) => None,
Type::Group(r#type) => should_validate_type(&r#type.elem),
Type::ImplTrait(r#type) => r#type.bounds.iter().any(
|bound| matches!(bound, TypeParamBound::Trait(bound) if is_validate_path(&bound.path)),
),
Type::Infer(_) => true,
Type::Macro(_) => true,
Type::Never(_) => false,
).then_some(KnownOrUnknown::Unknown),
Type::Infer(_) => Some(KnownOrUnknown::Unknown),
Type::Macro(_) => Some(KnownOrUnknown::Unknown),
Type::Never(_) => None,
Type::Paren(r#type) => should_validate_type(&r#type.elem),
Type::Path(r#type) => should_validate_path(&r#type.path),
Type::Ptr(r#type) => should_validate_type(&r#type.elem),
Type::Reference(r#type) => should_validate_type(&r#type.elem),
Type::Slice(r#type) => should_validate_type(&r#type.elem),
Type::TraitObject(r#type) => r#type.bounds.iter().any(
|bound| matches!(bound, TypeParamBound::Trait(bound) if is_validate_path(&bound.path)),
),
).then_some(KnownOrUnknown::Unknown),
Type::Tuple(r#type) => {
!r#type.elems.is_empty() && r#type.elems.iter().all(should_validate_type)
(!r#type.elems.is_empty() && r#type.elems.iter().all(|r#type| should_validate_type(r#type).is_some())).then_some(KnownOrUnknown::Unknown)
}
Type::Verbatim(_) => false,
_ => false,
Type::Verbatim(_) => None,
_ => None,
}
}

Expand All @@ -106,7 +142,7 @@ mod tests {
use super::should_validate_type;

fn validate(tokens: TokenStream) -> bool {
should_validate_type(&syn::parse2(tokens).expect("valid type"))
should_validate_type(&syn::parse2(tokens).expect("valid type")).is_some()
}

#[test]
Expand Down
37 changes: 28 additions & 9 deletions packages/fortifier-macros/src/validations/nested.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,49 @@
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Ident, Result, meta::ParseNestedMeta};
use quote::{ToTokens, format_ident, quote};
use syn::{Ident, Path, Result, meta::ParseNestedMeta};

use crate::validation::{Execution, Validation};

#[derive(Default)]
pub struct Nested {}
pub struct Nested {
error_type: TokenStream,
}

impl Nested {
pub fn new() -> Self {
Self {}
pub fn new(error_type: TokenStream) -> Self {
Self { error_type }
}
}

impl Validation for Nested {
fn parse(_meta: &ParseNestedMeta<'_>) -> Result<Self> {
unimplemented!()
fn parse(meta: &ParseNestedMeta<'_>) -> Result<Self> {
let mut error_type: Option<Path> = None;

meta.parse_nested_meta(|meta| {
if meta.path.is_ident("error_type") {
error_type = Some(meta.value()?.parse()?);

Ok(())
} else {
Err(meta.error("unknown parameter"))
}
})?;

let Some(error_type) = error_type else {
return Err(meta.error("missing `error_type` parameter"));
};

Ok(Nested {
error_type: error_type.to_token_stream(),
})
}

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

fn error_type(&self) -> TokenStream {
// TODO
quote!(::fortifier::NestedError)
self.error_type.clone()
}

fn expr(&self, execution: Execution, expr: &TokenStream) -> Option<TokenStream> {
Expand Down
Loading