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

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ fortifier = { path = "./packages/fortifier", version = "0.0.1" }
fortifier-macros = { path = "./packages/fortifier-macros", version = "0.0.1" }
indexmap = "2.12.0"
phonenumber = "0.3.7"
pretty_assertions = "1.4.1"
regex = "1.12.2"
serde = "1.0.228"
serde_json = "1.0.145"
Expand Down
2 changes: 2 additions & 0 deletions examples/server/src/email_address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod entities;
pub mod schemas;
12 changes: 12 additions & 0 deletions examples/server/src/email_address/entities.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
pub mod email_address {
use serde::Serialize;
use utoipa::ToSchema;
use uuid::Uuid;

#[derive(Serialize, ToSchema)]
pub struct Model {
pub id: Uuid,
pub email_addres: String,
pub label: String,
}
}
45 changes: 45 additions & 0 deletions examples/server/src/email_address/schemas.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use fortifier::Validate;
use serde::Deserialize;
use utoipa::ToSchema;
use uuid::Uuid;

#[derive(Deserialize, ToSchema, Validate)]
#[serde(rename_all = "camelCase")]
pub struct CreateEmailAddress {
#[validate(email_address)]
pub email_address: String,

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

#[derive(Deserialize, ToSchema, Validate)]
#[serde(rename_all = "camelCase")]
pub struct UpdateEmailAddress {
#[validate(email_address)]
pub email_address: Option<String>,

#[validate(length(min = 1, max = 256))]
pub label: Option<String>,
}

#[derive(Deserialize, ToSchema, Validate)]
#[serde(
tag = "type",
rename_all = "camelCase",
rename_all_fields = "camelCase"
)]
pub enum ChangeEmailAddressRelation {
Create(CreateEmailAddress),
Update {
#[validate(skip)]
id: Uuid,

#[serde(flatten)]
data: UpdateEmailAddress,
},
Delete {
#[validate(skip)]
id: Uuid,
},
}
1 change: 1 addition & 0 deletions examples/server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod email_address;
mod routes;
mod user;

Expand Down
1 change: 0 additions & 1 deletion examples/server/src/user/entities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ pub mod user {
#[derive(Serialize, ToSchema)]
pub struct Model {
pub id: Uuid,
pub email_address: String,
pub name: String,
}
}
166 changes: 154 additions & 12 deletions examples/server/src/user/routes.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
use axum::{Json, http::StatusCode, response::IntoResponse};
use axum::{Json, extract::Path, http::StatusCode, response::IntoResponse};
use fortifier::{Validate, ValidationErrors};
use serde::Deserialize;
use thiserror::Error;
use utoipa::IntoParams;
use utoipa_axum::{router::OpenApiRouter, routes};
use uuid::Uuid;

use crate::user::{
entities::user,
schemas::{CreateUser, CreateUserValidationError},
use crate::{
email_address::entities::email_address,
user::{
entities::user,
schemas::{
CreateUser, CreateUserValidationError, UpdateUser, UpdateUserValidationError,
UserWithEmailAddresses,
},
},
};

pub struct UserRoutes;
Expand All @@ -16,7 +24,9 @@ impl UserRoutes {
where
S: Clone + Send + Sync + 'static,
{
OpenApiRouter::new().routes(routes!(create_user))
OpenApiRouter::new()
.routes(routes!(create_user))
.routes(routes!(user, update_user, delete_user))
}
}

Expand All @@ -28,7 +38,11 @@ enum CreateUserError {

impl IntoResponse for CreateUserError {
fn into_response(self) -> axum::response::Response {
todo!()
match self {
CreateUserError::UnprocessableContent(errors) => {
(StatusCode::UNPROCESSABLE_ENTITY, Json(errors)).into_response()
}
}
}
}

Expand All @@ -41,21 +55,149 @@ impl IntoResponse for CreateUserError {
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),
(status = CREATED, description = "The created user.", body = UserWithEmailAddresses),
(status = UNPROCESSABLE_ENTITY, description = "Validation error.", body = ValidationErrors<CreateUserValidationError>),
)
)]
async fn create_user(
Json(data): Json<CreateUser>,
) -> Result<(StatusCode, Json<user::Model>), CreateUserError> {
) -> Result<(StatusCode, Json<UserWithEmailAddresses>), 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)))
let mut email_addresses = Vec::with_capacity(data.email_addresses.len());
for email_address_data in data.email_addresses {
email_addresses.push(email_address::Model {
id: Uuid::now_v7(),
email_addres: email_address_data.email_address,
label: email_address_data.label,
});
}

Ok((
StatusCode::CREATED,
Json(UserWithEmailAddresses {
model: user,
email_addresses,
}),
))
}

#[derive(Deserialize, IntoParams)]
#[serde(rename_all = "camelCase")]
pub struct UserPathParams {
user_id: Uuid,
}

#[utoipa::path(
get,
path = "/users/{userId}",
operation_id = "getUser",
summary = "Get user",
description = "Get a user.",
tags = ["User"],
params(
UserPathParams,
),
responses(
(status = OK, description = "The user.", body = UserWithEmailAddresses),
(status = NOT_FOUND, description = "Not found error."),
)
)]
async fn user(
Path(UserPathParams { user_id }): Path<UserPathParams>,
) -> Result<Json<UserWithEmailAddresses>, StatusCode> {
// TODO

let user = user::Model {
id: user_id,
name: "".to_owned(),
};

let email_addresses = vec![];

Ok(Json(UserWithEmailAddresses {
model: user,
email_addresses,
}))
}

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

impl IntoResponse for UpdateUserError {
fn into_response(self) -> axum::response::Response {
match self {
UpdateUserError::UnprocessableContent(errors) => {
(StatusCode::UNPROCESSABLE_ENTITY, Json(errors)).into_response()
}
}
}
}

#[utoipa::path(
patch,
path = "/users/{userId}",
operation_id = "updateUser",
summary = "Update user",
description = "Update a user.",
tags = ["User"],
params(
UserPathParams,
),
request_body = UpdateUser,
responses(
(status = OK, description = "The updated user.", body = UserWithEmailAddresses),
(status = UNPROCESSABLE_ENTITY, description = "Validation error.", body = ValidationErrors<UpdateUserValidationError>),
)
)]
async fn update_user(
Path(UserPathParams { user_id }): Path<UserPathParams>,
Json(data): Json<UpdateUser>,
) -> Result<Json<UserWithEmailAddresses>, UpdateUserError> {
data.validate().await?;

// TODO

let user = user::Model {
id: user_id,
name: "".to_owned(),
};

let email_addresses = vec![];

Ok(Json(UserWithEmailAddresses {
model: user,
email_addresses,
}))
}

#[utoipa::path(
delete,
path = "/users/{userId}",
operation_id = "deleteUser",
summary = "Delete user",
description = "Delete a user.",
tags = ["User"],
params(
UserPathParams,
),
responses(
(status = NO_CONTENT, description = "The user was deleted.",),
(status = NOT_FOUND, description = "Not found error."),
)
)]
async fn delete_user(
Path(UserPathParams { user_id: _user_id }): Path<UserPathParams>,
) -> Result<StatusCode, StatusCode> {
// TODO

Ok(StatusCode::NO_CONTENT)
}
35 changes: 32 additions & 3 deletions examples/server/src/user/schemas.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,41 @@
use fortifier::Validate;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

use crate::{
email_address::{
entities::email_address,
schemas::{
ChangeEmailAddressRelation, ChangeEmailAddressRelationValidationError,
CreateEmailAddress, CreateEmailAddressValidationError,
},
},
user::entities::user,
};

#[derive(Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UserWithEmailAddresses {
#[serde(flatten)]
pub model: user::Model,

pub email_addresses: Vec<email_address::Model>,
}

#[derive(Deserialize, ToSchema, Validate)]
#[serde(rename_all = "camelCase")]
pub struct CreateUser {
#[validate(email_address)]
pub email_address: String,
#[validate(length(min = 1, max = 256))]
pub name: String,

pub email_addresses: Vec<CreateEmailAddress>,
}

#[derive(Deserialize, ToSchema, Validate)]
#[serde(rename_all = "camelCase")]
pub struct UpdateUser {
#[validate(length(min = 1, max = 256))]
pub name: String,

pub email_addresses: Vec<ChangeEmailAddressRelation>,
}
9 changes: 8 additions & 1 deletion packages/fortifier-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,17 @@ syn = "2.0.110"

[dev-dependencies]
email_address.workspace = true
fortifier = { workspace = true, features = ["all-validations", "indexmap"] }
fortifier = { workspace = true, features = [
"all-validations",
"indexmap",
"serde",
] }
indexmap.workspace = true
phonenumber.workspace = true
pretty_assertions.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
trybuild = "1.0.114"
url.workspace = true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,23 @@ pub fn enum_attributes() -> TokenStream {
#( #attributes )*
}
}

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

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

if crate_name("serde").is_ok() {
attributes.push(quote! {
#[serde(with = "::fortifier::serde::errors")]
});
}
}

quote! {
#( #attributes )*
}
}
1 change: 1 addition & 0 deletions packages/fortifier-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

//! Fortifier macros.

mod attributes;
mod validate;
mod validation;
mod validations;
Expand Down
Loading
Loading