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
638 changes: 636 additions & 2 deletions Cargo.lock

Large diffs are not rendered by default.

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

[workspace.package]
Expand All @@ -16,6 +16,7 @@ regex = "1.12.2"
serde = "1.0.228"
serde_json = "1.0.145"
tokio = "1.48.0"
utoipa = "5.4.0"

[workspace.lints.rust]
unsafe_code = "deny"
Expand Down
6 changes: 4 additions & 2 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
all-features = true

[advisories]
ignore = []
ignore = [
{ id = "RUSTSEC-2024-0436", reason = "No maintained version available for `paste`." },
]

[bans]
allow-wildcard-paths = true
multiple-versions = "allow"
wildcards = "deny"

[licenses]
allow = ["MIT", "Unicode-3.0"]
allow = ["Apache-2.0", "BSD-3-Clause", "MIT", "Unicode-3.0"]
confidence-threshold = 1.0

[sources]
Expand Down
4 changes: 2 additions & 2 deletions example/Cargo.toml → examples/basic/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "fortifier-example"
description = "Fortifier example."
name = "fortifier-example-basic"
description = "Fortifier basic example."

authors.workspace = true
edition.workspace = true
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
29 changes: 29 additions & 0 deletions examples/server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "fortifier-example-server"
description = "Fortifier server example."

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

[dependencies]
axum = "0.8.7"
fortifier = { workspace = true, features = [
"email",
"regex",
"serde",
"url",
"utoipa",
] }
serde = { workspace = true, features = ["derive"] }
thiserror = "2.0.17"
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
utoipa = { workspace = true, features = ["axum_extras", "uuid"] }
utoipa-axum = "0.2.0"
utoipa-scalar = { version = "0.3.0", features = ["axum"] }
uuid = { version = "1.19.0", features = ["serde", "v7"] }

[lints]
workspace = true
24 changes: 24 additions & 0 deletions examples/server/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
mod routes;
mod user;

use std::{
error::Error,
net::{IpAddr, Ipv4Addr, SocketAddr},
};

use tokio::net::TcpListener;

use crate::routes::Routes;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let router = Routes::router();

let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080);
let listener = TcpListener::bind(&address).await?;

println!("listening on http://{}", &address);
axum::serve(listener, router).await?;

Ok(())
}
30 changes: 30 additions & 0 deletions examples/server/src/routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use axum::{Json, Router, routing::get};
use utoipa::OpenApi;
use utoipa_axum::router::OpenApiRouter;
use utoipa_scalar::{Scalar, Servable};

use crate::user::routes::UserRoutes;

#[derive(OpenApi)]
#[openapi(info(
title = "Fortifier Example API",
description = "Example to showcase Fortifier validation.",
))]
pub struct Routes;

impl Routes {
pub fn router<S>() -> Router<S>
where
S: Clone + Send + Sync + 'static,
{
let router = OpenApiRouter::new().merge(UserRoutes::router());

let (router, openapi) = OpenApiRouter::with_openapi(Routes::openapi())
.nest("/api/v1", router)
.split_for_parts();

router
.merge(Scalar::with_url("/api/reference", openapi.clone()))
.route("/api/v1/openapi.json", get(|| async { Json(openapi) }))
}
}
3 changes: 3 additions & 0 deletions examples/server/src/user.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod entities;
pub mod routes;
pub mod schemas;
12 changes: 12 additions & 0 deletions examples/server/src/user/entities.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
pub mod user {
use serde::Serialize;
use utoipa::ToSchema;
use uuid::Uuid;

#[derive(Serialize, ToSchema)]
pub struct Model {
pub id: Uuid,
pub email_address: String,
pub name: String,
}
}
61 changes: 61 additions & 0 deletions examples/server/src/user/routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use axum::{Json, http::StatusCode, response::IntoResponse};
use fortifier::{Validate, ValidationErrors};
use thiserror::Error;
use utoipa_axum::{router::OpenApiRouter, routes};
use uuid::Uuid;

use crate::user::{
entities::user,
schemas::{CreateUser, CreateUserValidationError},
};

pub struct UserRoutes;

impl UserRoutes {
pub fn router<S>() -> OpenApiRouter<S>
where
S: Clone + Send + Sync + 'static,
{
OpenApiRouter::new().routes(routes!(create_user))
}
}

#[derive(Debug, Error)]
enum CreateUserError {
#[error(transparent)]
UnprocessableContent(#[from] ValidationErrors<CreateUserValidationError>),
}

impl IntoResponse for CreateUserError {
fn into_response(self) -> axum::response::Response {
todo!()
}
}

#[utoipa::path(
post,
path = "/users",
operation_id = "createUser",
summary = "Create user",
description = "Create a user.",
tags = ["User"],
request_body = CreateUser,
responses(
(status = 201, description = "The created user.", body = user::Model),
(status = 400, description = "Validation error.", body = ValidationErrors<CreateUserValidationError>),
// (status = 500, description = "Internal server error.", body = ErrorBody),
)
)]
async fn create_user(
Json(data): Json<CreateUser>,
) -> Result<(StatusCode, Json<user::Model>), CreateUserError> {
data.validate().await?;

let user = user::Model {
id: Uuid::now_v7(),
email_address: data.email_address,
name: data.name,
};

Ok((StatusCode::CREATED, Json(user)))
}
12 changes: 12 additions & 0 deletions examples/server/src/user/schemas.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use fortifier::Validate;
use serde::Deserialize;
use utoipa::ToSchema;

#[derive(Deserialize, ToSchema, Validate)]
pub struct CreateUser {
#[validate(email)]
pub email_address: String,

#[validate(length(min = 1, max = 256))]
pub name: String,
}
6 changes: 6 additions & 0 deletions packages/fortifier-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ version.workspace = true
[lib]
proc-macro = true

[features]
default = []
serde = []
utoipa = []

[dependencies]
convert_case = "0.9.0"
proc-macro-crate = "3.4.0"
proc-macro2 = "1.0.103"
quote = "1.0.42"
syn = "2.0.110"
Expand Down
1 change: 1 addition & 0 deletions packages/fortifier-macros/src/validate.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod attributes;
mod r#enum;
mod field;
mod fields;
Expand Down
38 changes: 38 additions & 0 deletions packages/fortifier-macros/src/validate/attributes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use proc_macro2::TokenStream;
use quote::quote;

pub fn enum_attributes() -> TokenStream {
let mut attributes: Vec<TokenStream> = vec![];

Check warning on line 5 in packages/fortifier-macros/src/validate/attributes.rs

View workflow job for this annotation

GitHub Actions / Test

variable does not need to be mutable

Check warning on line 5 in packages/fortifier-macros/src/validate/attributes.rs

View workflow job for this annotation

GitHub Actions / Test

variable does not need to be mutable

Check warning on line 5 in packages/fortifier-macros/src/validate/attributes.rs

View workflow job for this annotation

GitHub Actions / Test

variable does not need to be mutable

Check warning on line 5 in packages/fortifier-macros/src/validate/attributes.rs

View workflow job for this annotation

GitHub Actions / Test

variable does not need to be mutable

Check warning on line 5 in packages/fortifier-macros/src/validate/attributes.rs

View workflow job for this annotation

GitHub Actions / Test

variable does not need to be mutable

Check warning on line 5 in packages/fortifier-macros/src/validate/attributes.rs

View workflow job for this annotation

GitHub Actions / Test

variable does not need to be mutable

Check warning on line 5 in packages/fortifier-macros/src/validate/attributes.rs

View workflow job for this annotation

GitHub Actions / Test

variable does not need to be mutable

Check warning on line 5 in packages/fortifier-macros/src/validate/attributes.rs

View workflow job for this annotation

GitHub Actions / Test

variable does not need to be mutable

Check warning on line 5 in packages/fortifier-macros/src/validate/attributes.rs

View workflow job for this annotation

GitHub Actions / Test

variable does not need to be mutable

Check warning on line 5 in packages/fortifier-macros/src/validate/attributes.rs

View workflow job for this annotation

GitHub Actions / Test

variable does not need to be mutable

#[cfg(feature = "serde")]
{
use proc_macro_crate::crate_name;

if crate_name("serde").is_ok() {
attributes.push(quote! {
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(
// TODO: Tag?
tag = "path",
rename_all = "camelCase",
rename_all_fields = "camelCase"
)]
});
}
}

#[cfg(feature = "utoipa")]
{
use proc_macro_crate::crate_name;

if crate_name("utoipa").is_ok() {
attributes.push(quote! {
#[derive(utoipa::ToSchema)]
});
}
}

quote! {
#( #attributes )*
}
}
3 changes: 3 additions & 0 deletions packages/fortifier-macros/src/validate/enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use syn::{DataEnum, DeriveInput, Generics, Ident, Result, Variant, Visibility};

use crate::{
validate::{
attributes::enum_attributes,
field::{LiteralOrIdent, ValidateFieldPrefix},
fields::ValidateFields,
},
Expand Down Expand Up @@ -44,6 +45,7 @@ impl ValidateEnum {
let visibility = &self.visibility;
let error_ident = &self.error_ident;

let attributes = enum_attributes();
let error_variant_idents = self
.variants
.iter()
Expand All @@ -60,6 +62,7 @@ impl ValidateEnum {
quote! {
#[allow(dead_code)]
#[derive(Debug)]
#attributes
#visibility enum #error_ident {
#( #error_variant_idents(#error_variant_types) ),*
}
Expand Down
3 changes: 3 additions & 0 deletions packages/fortifier-macros/src/validate/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use quote::{ToTokens, format_ident, quote};
use syn::{Field, Ident, Result, Visibility};

use crate::{
validate::attributes::enum_attributes,
validation::{Execution, Validation},
validations::{Custom, Email, Length, Regex, Url},
};
Expand Down Expand Up @@ -103,6 +104,7 @@ impl ValidateField {

pub fn error_type(&self, ident: &Ident) -> (TokenStream, Option<TokenStream>) {
if self.validations.len() > 1 {
let attributes = enum_attributes();
let visibility = &self.visibility;
let ident = format_ident!("{}{}ValidationError", ident, self.error_ident);
let variant_ident = self.validations.iter().map(|validation| validation.ident());
Expand All @@ -115,6 +117,7 @@ impl ValidateField {
ident.to_token_stream(),
Some(quote! {
#[derive(Debug)]
#attributes
#visibility enum #ident {
#( #variant_ident(#variant_type) ),*
}
Expand Down
8 changes: 7 additions & 1 deletion packages/fortifier-macros/src/validate/fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ use quote::{ToTokens, format_ident, quote};
use syn::{Fields, FieldsNamed, FieldsUnnamed, Ident, Result, Visibility};

use crate::{
validate::field::{LiteralOrIdent, ValidateField, ValidateFieldPrefix},
validate::{
attributes::enum_attributes,
field::{LiteralOrIdent, ValidateField, ValidateFieldPrefix},
},
validation::Execution,
};

Expand Down Expand Up @@ -194,6 +197,8 @@ fn error_type<'a>(
error_ident: &Ident,
fields: impl Iterator<Item = &'a ValidateField>,
) -> (TokenStream, TokenStream) {
let attributes = enum_attributes();

let mut error_field_idents = vec![];
let mut error_field_types = vec![];
let mut error_field_enums = vec![];
Expand All @@ -214,6 +219,7 @@ fn error_type<'a>(
quote! {
#[allow(dead_code)]
#[derive(Debug)]
#attributes
#visibility enum #error_ident {
#( #error_field_idents(#error_field_types) ),*
}
Expand Down
4 changes: 3 additions & 1 deletion packages/fortifier/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ indexmap = ["dep:indexmap"]
macros = ["dep:fortifier-macros"]
message = []
regex = ["dep:regex"]
serde = ["dep:serde", "email_address/serde_support"]
serde = ["dep:serde", "email_address?/serde_support", "fortifier-macros?/serde"]
url = ["dep:url"]
utoipa = ["dep:utoipa", "fortifier-macros?/utoipa"]

[dependencies]
email_address = { version = "0.2.9", default-features = false, optional = true }
Expand All @@ -25,6 +26,7 @@ indexmap = { version = "2.12.0", optional = true }
regex = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"], optional = true }
url = { version = "2.5.7", optional = true }
utoipa = { workspace = true, optional = true }

[dev-dependencies]
pretty_assertions = "1.4.1"
Expand Down
9 changes: 9 additions & 0 deletions packages/fortifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@ pub use validations::*;

#[cfg(feature = "macros")]
pub use fortifier_macros::*;

#[doc(hidden)]
pub mod external {
#[cfg(feature = "serde")]
pub use serde;

#[cfg(feature = "utoipa")]
pub use utoipa;
}
1 change: 1 addition & 0 deletions packages/fortifier/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::{
/// Validation errors.
#[derive(Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ValidationErrors<E>(Vec<E>);

impl<E: Debug> Display for ValidationErrors<E> {
Expand Down
Loading
Loading