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
24 changes: 24 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["packages/*"]
members = ["example", "packages/*"]
resolver = "2"

[workspace.package]
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Schema validation.

See [the Fortifier book](https://fortifier.rustforweb.org/) for documentation.

## Credits

Inspired by [`validator`](https://github.com/Keats/validator).

## License

This project is available under the [MIT license](LICENSE.md).
Expand Down
1 change: 0 additions & 1 deletion book/book.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[book]
authors = ["Daniëlle Huisman"]
language = "en"
multilingual = false
src = "src"
title = "Fortifier"

Expand Down
4 changes: 4 additions & 0 deletions book/src/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Schema validation.

## Credits

Inspired by [`validator`](https://github.com/Keats/validator).

## License

This project is available under the [MIT license](https://github.com/RustForWeb/fortifier/blob/main/LICENSE.md).
Expand Down
12 changes: 12 additions & 0 deletions example/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "fortifier-example"
description = "Fortifier example."

authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true

[dependencies]
fortifier.workspace = true
23 changes: 23 additions & 0 deletions example/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use std::error::Error;

use fortifier::Validate;

#[derive(Validate)]
struct CreateUser {
#[validate(email)]
email: String,

#[validate(length(min = 1, max = 256))]
name: String,
}

fn main() -> Result<(), Box<dyn Error>> {
let data = CreateUser {
email: "john@doe.com".to_owned(),
name: "John Doe".to_owned(),
};

data.validate_sync()?;

Ok(())
}
1 change: 1 addition & 0 deletions packages/fortifier-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ version.workspace = true
proc-macro = true

[dependencies]
convert_case = "0.9.0"
proc-macro2 = "1.0.103"
quote = "1.0.42"
syn = "2.0.110"
Expand Down
47 changes: 0 additions & 47 deletions packages/fortifier-macros/src/derive.rs

This file was deleted.

11 changes: 7 additions & 4 deletions packages/fortifier-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
mod derive;
mod validate;
mod validations;

use proc_macro::TokenStream;
use syn::{DeriveInput, parse_macro_input};
use syn::{DeriveInput, Error, parse_macro_input};

use crate::derive::validate_tokens;
use crate::validate::validate_tokens;

#[proc_macro_derive(Validate, attributes(validate))]
pub fn derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);

validate_tokens(input).into()
validate_tokens(input)
.unwrap_or_else(Error::into_compile_error)
.into()
}
22 changes: 22 additions & 0 deletions packages/fortifier-macros/src/validate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
mod r#enum;
mod r#struct;
mod r#union;

use proc_macro2::TokenStream;
use quote::format_ident;
use syn::{Data, DeriveInput, Result};

use crate::validate::{
r#enum::validate_enum, r#struct::validate_struct_tokens, union::validate_union,
};

pub fn validate_tokens(input: DeriveInput) -> Result<TokenStream> {
let ident = input.ident;
let error_ident = format_ident!("{ident}ValidationError");

match input.data {
Data::Struct(data) => validate_struct_tokens(ident, error_ident, data),
Data::Enum(data) => validate_enum(ident, error_ident, data),
Data::Union(data) => validate_union(ident, error_ident, data),
}
}
6 changes: 6 additions & 0 deletions packages/fortifier-macros/src/validate/enum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use proc_macro2::TokenStream;
use syn::{DataEnum, Ident, Result};

pub fn validate_enum(_ident: Ident, _error_ident: Ident, _data: DataEnum) -> Result<TokenStream> {
todo!("enum")
}
130 changes: 130 additions & 0 deletions packages/fortifier-macros/src/validate/struct.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use convert_case::{Case, Casing};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{DataStruct, Field, Fields, Ident, Result};

use crate::validations::{email_tokens, length_tokens, parse_email, parse_length};

pub fn validate_struct_tokens(
ident: Ident,
error_ident: Ident,
data: DataStruct,
) -> Result<TokenStream> {
match data.fields {
Fields::Named(fields_named) => {
validate_named_struct_tokens(ident, error_ident, fields_named.named.into_iter())
}
Fields::Unnamed(_fields_unnamed) => todo!("fields unamed"),
Fields::Unit => todo!("fields unit"),
}
}

fn validate_named_struct_tokens(
ident: Ident,
error_ident: Ident,
fields: impl Iterator<Item = Field>,
) -> Result<TokenStream> {
let mut field_names = vec![];
let mut field_types = vec![];
let mut sync_validations = vec![];
// let async_validations = vec![];

for field in fields {
let Some(field_ident) = field.ident else {
continue;
};

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

for attr in field.attrs {
if attr.path().is_ident("validate") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("email") {
let email = parse_email(&meta)?;

field_names.push(field_error_ident.clone());
field_types.push(quote!(::fortifier::EmailError));

sync_validations.push(email_tokens(
email,
&error_ident,
&field_ident,
&field_error_ident,
));

Ok(())
} else if meta.path.is_ident("length") {
let length = parse_length(&meta)?;

field_names.push(field_error_ident.clone());
field_types.push(quote!(::fortifier::LengthError<usize>));

sync_validations.push(length_tokens(
length,
&error_ident,
&field_ident,
&field_error_ident,
));

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

Ok(quote! {
use fortifier::*;

#[derive(Debug)]
enum #error_ident {
#( #field_names(#field_types) ),*
}

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

impl ::std::error::Error for #error_ident {}

impl Validate for #ident {
type Error = #error_ident;

fn validate_sync(&self) -> Result<(), ValidationErrors<Self::Error>> {
use ::fortifier::*;

let mut errors = vec![];

#(#sync_validations)*

if !errors.is_empty() {
Err(errors.into())
} else {
Ok(())
}
}

fn validate_async(&self) -> ::std::pin::Pin<Box<impl Future<Output = Result<(), ValidationErrors<Self::Error>>>>> {
use ::fortifier::*;

Box::pin(async {

let mut errors = vec![];

// #(#async_validations)*

if !errors.is_empty() {
Err(errors.into())
} else {
Ok(())
}
})
}
}
})
}
6 changes: 6 additions & 0 deletions packages/fortifier-macros/src/validate/union.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use proc_macro2::TokenStream;
use syn::{DataUnion, Ident, Result};

pub fn validate_union(_ident: Ident, _error_ident: Ident, _data: DataUnion) -> Result<TokenStream> {
todo!("union")
}
5 changes: 5 additions & 0 deletions packages/fortifier-macros/src/validations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod email;
mod length;

pub use email::*;
pub use length::*;
23 changes: 23 additions & 0 deletions packages/fortifier-macros/src/validations/email.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Ident, Result, meta::ParseNestedMeta};

#[derive(Default)]
pub struct Email {}

pub fn parse_email(_meta: &ParseNestedMeta<'_>) -> Result<Email> {
Ok(Email::default())
}

pub fn email_tokens(
_email: Email,
error_ident: &Ident,
field_ident: &Ident,
field_error_ident: &Ident,
) -> TokenStream {
quote! {
if let Err(err) = self.#field_ident.validate_email() {
errors.push(#error_ident::#field_error_ident(err));
}
}
}
Loading
Loading