diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index f67c504..635b056 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -134,6 +134,7 @@ jobs: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - name: Upload coverage reports to DeepSource + if: github.event_name == 'pull_request' run: | curl https://deepsource.io/cli | sh ./bin/deepsource report --analyzer test-coverage --key rust --value-file ./lcov.info diff --git a/link-for-later/src/app.rs b/link-for-later/src/app.rs index b15ae44..7766492 100644 --- a/link-for-later/src/app.rs +++ b/link-for-later/src/app.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{error, fmt, sync::Arc}; use axum::Router; @@ -6,7 +6,6 @@ use crate::{ controller, repository, repository::{DynLinks as DynLinksRepository, DynUsers as DynUsersRepository}, service::{self, DynLinks as DynLinksService, DynUsers as DynUsersService}, - state::AppState, types::Database, }; @@ -26,9 +25,85 @@ pub fn new(db: Database) -> Router { ), }; - let state = AppState::new(links_service, users_service, links_repo, users_repo); + let state = State::new(links_service, users_service, links_repo, users_repo); Router::new() - .merge(controller::links::routes(state.clone())) - .merge(controller::users::routes(state.clone())) + .merge(controller::routes::links::router(state.clone())) + .merge(controller::routes::users::router(state.clone())) .with_state(state) } + +#[derive(Clone)] +pub struct State { + links_service: DynLinksService, + users_service: DynUsersService, + links_repo: DynLinksRepository, + users_repo: DynUsersRepository, +} + +#[allow(clippy::must_use_candidate)] +impl State { + pub fn new( + links_service: DynLinksService, + users_service: DynUsersService, + links_repo: DynLinksRepository, + users_repo: DynUsersRepository, + ) -> Self { + Self { + links_service, + users_service, + links_repo, + users_repo, + } + } + + pub fn links_service(&self) -> &DynLinksService { + &self.links_service + } + + pub fn users_service(&self) -> &DynUsersService { + &self.users_service + } + + pub fn links_repo(&self) -> &DynLinksRepository { + &self.links_repo + } + + pub fn users_repo(&self) -> &DynUsersRepository { + &self.users_repo + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Error { + LinkNotFound(String), + UserAlreadyExists(String), + UserNotFound(String), + IncorrectPassword(String), + Authorization(String), + Validation(String), + Database(String), + Server(String), + + #[cfg(test)] + Test, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::LinkNotFound(_) => write!(f, "link item not found"), + Self::UserAlreadyExists(_) => write!(f, "user already registered"), + Self::UserNotFound(_) => write!(f, "user not found"), + Self::IncorrectPassword(_) => write!(f, "incorrect password for user"), + Self::Authorization(_) => write!(f, "invalid authorization token"), + Self::Validation(_) => write!(f, "invalid request"), + Self::Database(_) => write!(f, "database error"), + Self::Server(_) => write!(f, "server error"), + + #[cfg(test)] + Self::Test => write!(f, "test error"), + } + } +} + +impl error::Error for Error {} diff --git a/link-for-later/src/controller.rs b/link-for-later/src/controller.rs index 8d6299b..e144a9f 100644 --- a/link-for-later/src/controller.rs +++ b/link-for-later/src/controller.rs @@ -1,4 +1,3 @@ -pub mod auth; -pub mod error; -pub mod links; -pub mod users; +pub mod extractors; +pub mod responses; +pub mod routes; diff --git a/link-for-later/src/controller/auth.rs b/link-for-later/src/controller/extractors.rs similarity index 82% rename from link-for-later/src/controller/auth.rs rename to link-for-later/src/controller/extractors.rs index ee60fb7..3f4547d 100644 --- a/link-for-later/src/controller/auth.rs +++ b/link-for-later/src/controller/extractors.rs @@ -5,7 +5,7 @@ use axum_extra::{ }; use jsonwebtoken::{decode, DecodingKey, Validation}; -use crate::types::{auth::Claims, AppError}; +use crate::{dto::Claims, types::AppError}; const JWT_SECRET_KEY: &str = "JWT_SECRET"; @@ -21,7 +21,7 @@ where TypedHeader::>::from_request_parts(parts, state) .await .map_err(|_| { - AppError::AuthorizationError(String::from("Authorization token not found")) + AppError::Authorization(String::from("Authorization token not found")) })?; let secret = @@ -31,7 +31,7 @@ where &DecodingKey::from_secret(secret.as_bytes()), &Validation::default(), ) - .map_err(|e| AppError::AuthorizationError(format!("decode() {e:?}")))?; + .map_err(|e| AppError::Authorization(format!("decode() {e:?}")))?; Ok(token_data.claims) } diff --git a/link-for-later/src/controller/error.rs b/link-for-later/src/controller/responses.rs similarity index 85% rename from link-for-later/src/controller/error.rs rename to link-for-later/src/controller/responses.rs index c04dc6e..504563b 100644 --- a/link-for-later/src/controller/error.rs +++ b/link-for-later/src/controller/responses.rs @@ -13,14 +13,6 @@ impl IntoResponse for AppError { let error_message = self.to_string(); tracing::info!("{}", error_message); let (status, error_message) = match self { - Self::ServerError(ref e) => { - tracing::debug!("{}: {}", error_message, e.to_string()); - (StatusCode::INTERNAL_SERVER_ERROR, error_message) - } - Self::DatabaseError(ref e) => { - tracing::debug!("{}: {}", error_message, e.to_string()); - (StatusCode::INTERNAL_SERVER_ERROR, error_message) - } Self::LinkNotFound(ref e) => { tracing::debug!("{}: {}", error_message, e.to_string()); (StatusCode::NOT_FOUND, error_message) @@ -37,17 +29,25 @@ impl IntoResponse for AppError { tracing::debug!("{}: {}", error_message, e.to_string()); (StatusCode::UNAUTHORIZED, error_message) } - Self::AuthorizationError(ref e) => { + Self::Authorization(ref e) => { tracing::debug!("{}: {}", error_message, e.to_string()); (StatusCode::UNAUTHORIZED, error_message) } - Self::ValidationError(ref e) => { + Self::Validation(ref e) => { tracing::debug!("{}: {}", error_message, e.to_string()); (StatusCode::BAD_REQUEST, error_message) } + Self::Database(ref e) => { + tracing::debug!("{}: {}", error_message, e.to_string()); + (StatusCode::INTERNAL_SERVER_ERROR, error_message) + } + Self::Server(ref e) => { + tracing::debug!("{}: {}", error_message, e.to_string()); + (StatusCode::INTERNAL_SERVER_ERROR, error_message) + } #[cfg(test)] - Self::TestError => (StatusCode::INTERNAL_SERVER_ERROR, error_message), + Self::Test => (StatusCode::INTERNAL_SERVER_ERROR, error_message), }; let body = Json(json!({ @@ -66,13 +66,13 @@ mod tests { #[test] fn test_error_response() { assert_eq!( - AppError::ServerError("a server operation failed".into()) + AppError::Server("a server operation failed".into()) .into_response() .status(), StatusCode::INTERNAL_SERVER_ERROR ); assert_eq!( - AppError::DatabaseError("a database operation failed".into()) + AppError::Database("a database operation failed".into()) .into_response() .status(), StatusCode::INTERNAL_SERVER_ERROR @@ -102,13 +102,13 @@ mod tests { StatusCode::UNAUTHORIZED ); assert_eq!( - AppError::AuthorizationError("an authorization error occurred".into()) + AppError::Authorization("an authorization error occurred".into()) .into_response() .status(), StatusCode::UNAUTHORIZED ); assert_eq!( - AppError::ValidationError("a validation error occurred".into()) + AppError::Validation("a validation error occurred".into()) .into_response() .status(), StatusCode::BAD_REQUEST diff --git a/link-for-later/src/controller/routes.rs b/link-for-later/src/controller/routes.rs new file mode 100644 index 0000000..0f971ee --- /dev/null +++ b/link-for-later/src/controller/routes.rs @@ -0,0 +1,2 @@ +pub mod links; +pub mod users; diff --git a/link-for-later/src/controller/links.rs b/link-for-later/src/controller/routes/links.rs similarity index 95% rename from link-for-later/src/controller/links.rs rename to link-for-later/src/controller/routes/links.rs index 044b809..a9257cb 100644 --- a/link-for-later/src/controller/links.rs +++ b/link-for-later/src/controller/routes/links.rs @@ -7,16 +7,12 @@ use axum::{ use validator::Validate; use crate::{ - state::AppState, - types::{ - auth::Claims, - dto::{LinkItemRequest, LinkQueryBuilder}, - entity::LinkItemBuilder, - AppError, - }, + dto::{Claims, LinkItemRequest, LinkQueryBuilder}, + entity::LinkItemBuilder, + types::{AppError, AppState}, }; -pub fn routes(state: AppState) -> Router { +pub fn router(state: AppState) -> Router { Router::new() .nest( "/v1", @@ -50,7 +46,7 @@ async fn post( match payload.validate() { Ok(()) => {} Err(e) => { - return AppError::ValidationError(format!("post_link() {e:?}")).into_response(); + return AppError::Validation(format!("post_link() {e:?}")).into_response(); } } @@ -95,7 +91,7 @@ async fn put( match payload.validate() { Ok(()) => {} Err(e) => { - return AppError::ValidationError(format!("put_link() {e:?}")).into_response(); + return AppError::Validation(format!("put_link() {e:?}")).into_response(); } } @@ -141,13 +137,12 @@ mod tests { use tracing_test::traced_test; use crate::{ + entity::LinkItem, repository::{MockLinks as MockLinksRepo, MockUsers as MockUsersRepo}, service::{ DynLinks as DynLinksService, MockLinks as MockLinksService, MockUsers as MockUsersService, }, - state::AppState, - types::{auth::Claims, entity::LinkItem}, }; use super::*; @@ -214,7 +209,7 @@ mod tests { .expect_search() .withf(move |_, query| query == &repo_query) .times(1) - .returning(|_, _| Err(AppError::TestError)); + .returning(|_, _| Err(AppError::Test)); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = list(State(app_state), Claims::new("user-id", 0, 0)).await; @@ -298,7 +293,7 @@ mod tests { .expect_create() .withf(move |_, item| item == &item_to_create) .times(1) - .returning(|_, _| Err(AppError::TestError)); + .returning(|_, _| Err(AppError::Test)); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = post( @@ -361,7 +356,7 @@ mod tests { .expect_get() .withf(move |_, query| query == &repo_query) .times(1) - .returning(|_, _| Err(AppError::TestError)); + .returning(|_, _| Err(AppError::Test)); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = get( @@ -458,7 +453,7 @@ mod tests { .expect_update() .withf(move |_, id, item| id == "1" && item == &item_to_update) .times(1) - .returning(|_, _, _| Err(AppError::TestError)); + .returning(|_, _, _| Err(AppError::Test)); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = put( @@ -511,7 +506,7 @@ mod tests { .expect_delete() .withf(move |_, item| item == &item_to_delete) .times(1) - .returning(|_, _| Err(AppError::TestError)); + .returning(|_, _| Err(AppError::Test)); let app_state = AppStateBuilder::new(Arc::new(mock_links_service)).build(); let response = delete( diff --git a/link-for-later/src/controller/users.rs b/link-for-later/src/controller/routes/users.rs similarity index 92% rename from link-for-later/src/controller/users.rs rename to link-for-later/src/controller/routes/users.rs index aaeb938..e7346ba 100644 --- a/link-for-later/src/controller/users.rs +++ b/link-for-later/src/controller/routes/users.rs @@ -2,15 +2,12 @@ use axum::{extract::State, http::StatusCode, response::IntoResponse, routing, Js use validator::Validate; use crate::{ - state::AppState, - types::{ - dto::{AuthResponse, UserInfoRequest}, - entity::UserInfoBuilder, - AppError, - }, + dto::{LoginResponse, UserInfoRequest}, + entity::UserInfoBuilder, + types::{AppError, AppState}, }; -pub fn routes(state: AppState) -> Router { +pub fn router(state: AppState) -> Router { Router::new() .nest( "/v1", @@ -31,7 +28,7 @@ async fn register( match payload.validate() { Ok(()) => {} Err(e) => { - return AppError::ValidationError(format!("register() {e:?}")).into_response(); + return AppError::Validation(format!("register() {e:?}")).into_response(); } } @@ -54,7 +51,7 @@ async fn login( match payload.validate() { Ok(()) => {} Err(e) => { - return AppError::ValidationError(format!("login() {e:?}")).into_response(); + return AppError::Validation(format!("login() {e:?}")).into_response(); } } @@ -66,7 +63,7 @@ async fn login( .await { Ok(token) => { - let response = AuthResponse::new(token.jwt()); + let response = LoginResponse::new(token.jwt()); (StatusCode::OK, Json(response)).into_response() } Err(e) => e.into_response(), @@ -82,13 +79,12 @@ mod tests { use tracing_test::traced_test; use crate::{ + dto::Token, repository::{MockLinks as MockLinksRepo, MockUsers as MockUsersRepo}, service::{ DynUsers as DynUsersService, MockLinks as MockLinksService, MockUsers as MockUsersService, }, - state::AppState, - types::auth::Token, }; use super::*; @@ -147,7 +143,7 @@ mod tests { .expect_register() .withf(move |_, user| user == &user_to_register) .times(1) - .returning(|_, _| Err(AppError::TestError)); + .returning(|_, _| Err(AppError::Test)); let app_state = AppStateBuilder::new(Arc::new(mock_users_service)).build(); let response = register(State(app_state), Json(request)).await; @@ -215,7 +211,7 @@ mod tests { .expect_login() .withf(move |_, user| user == &user_to_login) .times(1) - .returning(|_, _| Err(AppError::TestError)); + .returning(|_, _| Err(AppError::Test)); let app_state = AppStateBuilder::new(Arc::new(mock_users_service)).build(); let response = login(State(app_state), Json(request)).await; diff --git a/link-for-later/src/dto.rs b/link-for-later/src/dto.rs new file mode 100644 index 0000000..6beaf49 --- /dev/null +++ b/link-for-later/src/dto.rs @@ -0,0 +1,12 @@ +pub use auth::{Claims, Token}; +pub use request::links::{ + Item as LinkItemRequest, Query as LinkQuery, QueryBuilder as LinkQueryBuilder, +}; +pub use request::users::{ + Info as UserInfoRequest, Query as UserQuery, QueryBuilder as UserQueryBuilder, +}; +pub use response::Login as LoginResponse; + +mod auth; +mod request; +mod response; diff --git a/link-for-later/src/types/auth.rs b/link-for-later/src/dto/auth.rs similarity index 100% rename from link-for-later/src/types/auth.rs rename to link-for-later/src/dto/auth.rs index af5911f..d57613e 100644 --- a/link-for-later/src/types/auth.rs +++ b/link-for-later/src/dto/auth.rs @@ -7,11 +7,6 @@ pub struct Claims { exp: usize, // expiration time } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct Token { - jwt: String, -} - impl Claims { pub fn new(sub: &str, iat: usize, exp: usize) -> Self { Self { @@ -26,6 +21,11 @@ impl Claims { } } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct Token { + jwt: String, +} + impl Token { pub fn new(jwt: &str) -> Self { Self { diff --git a/link-for-later/src/dto/request.rs b/link-for-later/src/dto/request.rs new file mode 100644 index 0000000..0f971ee --- /dev/null +++ b/link-for-later/src/dto/request.rs @@ -0,0 +1,2 @@ +pub mod links; +pub mod users; diff --git a/link-for-later/src/dto/request/links.rs b/link-for-later/src/dto/request/links.rs new file mode 100644 index 0000000..2adebd3 --- /dev/null +++ b/link-for-later/src/dto/request/links.rs @@ -0,0 +1,79 @@ +use serde::{Deserialize, Serialize}; +use validator::Validate; + +#[derive(Default, Serialize, Deserialize, Validate)] +pub struct Item { + #[validate(url)] + url: String, + #[serde(default = "String::default")] + title: String, + #[serde(default = "String::default")] + description: String, +} + +impl Item { + pub fn url(&self) -> &str { + &self.url + } + + pub fn title(&self) -> &str { + &self.title + } + + pub fn description(&self) -> &str { + &self.description + } + + #[cfg(test)] + pub fn new(url: &str) -> Self { + Self { + url: url.to_string(), + ..Default::default() + } + } +} + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Query { + #[serde(skip_serializing_if = "String::is_empty")] + id: String, + #[serde(skip_serializing_if = "String::is_empty")] + owner: String, +} + +impl Query { + pub fn id(&self) -> &str { + &self.id + } + + pub fn owner(&self) -> &str { + &self.owner + } +} + +#[derive(Default)] +pub struct QueryBuilder { + id: String, + owner: String, +} + +impl QueryBuilder { + pub fn new(id: &str, owner: &str) -> Self { + Self { + id: id.to_string(), + owner: owner.to_string(), + } + } + + pub fn owner(mut self, owner: &str) -> Self { + self.owner = owner.to_string(); + self + } + + pub fn build(self) -> Query { + Query { + id: self.id, + owner: self.owner, + } + } +} diff --git a/link-for-later/src/dto/request/users.rs b/link-for-later/src/dto/request/users.rs new file mode 100644 index 0000000..6a959cd --- /dev/null +++ b/link-for-later/src/dto/request/users.rs @@ -0,0 +1,55 @@ +use serde::{Deserialize, Serialize}; +use validator::Validate; + +#[derive(Serialize, Deserialize, Validate)] +pub struct Info { + #[validate(email)] + email: String, + password: String, +} + +impl Info { + pub fn email(&self) -> &str { + &self.email + } + + pub fn password(&self) -> &str { + &self.password + } + + #[cfg(test)] + pub fn new(email: &str, password: &str) -> Self { + Self { + email: email.to_string(), + password: password.to_string(), + } + } +} + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Query { + email: String, +} + +impl Query { + pub fn email(&self) -> &str { + &self.email + } +} + +#[derive(Default)] +pub struct QueryBuilder { + email: String, +} + +impl QueryBuilder { + pub fn new(email: &str) -> Self { + Self { + email: email.to_string(), + } + } + + pub fn build(self) -> Query { + Query { email: self.email } + } +} diff --git a/link-for-later/src/dto/response.rs b/link-for-later/src/dto/response.rs new file mode 100644 index 0000000..bba4713 --- /dev/null +++ b/link-for-later/src/dto/response.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Login { + token: String, +} + +impl Login { + pub fn new(token: &str) -> Self { + Self { + token: token.to_string(), + } + } +} diff --git a/link-for-later/src/entity.rs b/link-for-later/src/entity.rs new file mode 100644 index 0000000..6697dda --- /dev/null +++ b/link-for-later/src/entity.rs @@ -0,0 +1,5 @@ +pub use links::{Item as LinkItem, ItemBuilder as LinkItemBuilder}; +pub use users::{Info as UserInfo, InfoBuilder as UserInfoBuilder}; + +mod links; +mod users; diff --git a/link-for-later/src/types/entity.rs b/link-for-later/src/entity/links.rs similarity index 50% rename from link-for-later/src/types/entity.rs rename to link-for-later/src/entity/links.rs index 8a4c479..3891aab 100644 --- a/link-for-later/src/types/entity.rs +++ b/link-for-later/src/entity/links.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct LinkItem { +pub struct Item { id: String, owner: String, url: String, @@ -11,7 +11,7 @@ pub struct LinkItem { updated_at: String, } -impl LinkItem { +impl Item { pub fn id(&self) -> &str { &self.id } @@ -31,7 +31,7 @@ impl LinkItem { } #[derive(Default)] -pub struct LinkItemBuilder { +pub struct ItemBuilder { id: String, owner: String, url: String, @@ -41,7 +41,7 @@ pub struct LinkItemBuilder { updated_at: String, } -impl LinkItemBuilder { +impl ItemBuilder { pub fn new(url: &str) -> Self { Self { url: url.to_string(), @@ -84,8 +84,8 @@ impl LinkItemBuilder { self } - pub fn build(self) -> LinkItem { - LinkItem { + pub fn build(self) -> Item { + Item { id: self.id, owner: self.owner, url: self.url, @@ -97,8 +97,8 @@ impl LinkItemBuilder { } } -impl From for LinkItemBuilder { - fn from(item: LinkItem) -> Self { +impl From for ItemBuilder { + fn from(item: Item) -> Self { Self { id: item.id, owner: item.owner, @@ -110,88 +110,3 @@ impl From for LinkItemBuilder { } } } - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct UserInfo { - id: String, - email: String, - password: String, - verified: bool, - created_at: String, - updated_at: String, -} - -impl UserInfo { - pub fn email(&self) -> &str { - &self.email - } - - pub fn password(&self) -> &str { - &self.password - } -} - -#[derive(Default)] -pub struct UserInfoBuilder { - id: String, - email: String, - password: String, - verified: bool, - created_at: String, - updated_at: String, -} - -impl UserInfoBuilder { - pub fn new(email: &str, password: &str) -> Self { - Self { - email: email.to_string(), - password: password.to_string(), - ..Default::default() - } - } - - pub fn id(mut self, id: &str) -> Self { - self.id = id.to_string(); - self - } - - #[allow(clippy::missing_const_for_fn)] - pub fn verified(mut self, verified: bool) -> Self { - self.verified = verified; - self - } - - pub fn created_at(mut self, created_at: &str) -> Self { - self.created_at = created_at.to_string(); - self - } - - pub fn updated_at(mut self, updated_at: &str) -> Self { - self.updated_at = updated_at.to_string(); - self - } - - pub fn build(self) -> UserInfo { - UserInfo { - id: self.id, - email: self.email, - password: self.password, - verified: self.verified, - created_at: self.created_at, - updated_at: self.updated_at, - } - } -} - -impl From for UserInfoBuilder { - fn from(info: UserInfo) -> Self { - Self { - id: info.id, - email: info.email, - password: info.password, - verified: info.verified, - created_at: info.created_at, - updated_at: info.updated_at, - } - } -} diff --git a/link-for-later/src/entity/users.rs b/link-for-later/src/entity/users.rs new file mode 100644 index 0000000..4018386 --- /dev/null +++ b/link-for-later/src/entity/users.rs @@ -0,0 +1,86 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct Info { + id: String, + email: String, + password: String, + verified: bool, + created_at: String, + updated_at: String, +} + +impl Info { + pub fn email(&self) -> &str { + &self.email + } + + pub fn password(&self) -> &str { + &self.password + } +} + +#[derive(Default)] +pub struct InfoBuilder { + id: String, + email: String, + password: String, + verified: bool, + created_at: String, + updated_at: String, +} + +impl InfoBuilder { + pub fn new(email: &str, password: &str) -> Self { + Self { + email: email.to_string(), + password: password.to_string(), + ..Default::default() + } + } + + pub fn id(mut self, id: &str) -> Self { + self.id = id.to_string(); + self + } + + #[allow(clippy::missing_const_for_fn)] + pub fn verified(mut self, verified: bool) -> Self { + self.verified = verified; + self + } + + pub fn created_at(mut self, created_at: &str) -> Self { + self.created_at = created_at.to_string(); + self + } + + pub fn updated_at(mut self, updated_at: &str) -> Self { + self.updated_at = updated_at.to_string(); + self + } + + pub fn build(self) -> Info { + Info { + id: self.id, + email: self.email, + password: self.password, + verified: self.verified, + created_at: self.created_at, + updated_at: self.updated_at, + } + } +} + +impl From for InfoBuilder { + fn from(info: Info) -> Self { + Self { + id: info.id, + email: info.email, + password: info.password, + verified: info.verified, + created_at: info.created_at, + updated_at: info.updated_at, + } + } +} diff --git a/link-for-later/src/lib.rs b/link-for-later/src/lib.rs index f9a0840..502aead 100644 --- a/link-for-later/src/lib.rs +++ b/link-for-later/src/lib.rs @@ -1,10 +1,11 @@ #![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] -pub use crate::types::Database as DatabaseType; +pub use types::Database as DatabaseType; pub mod app; mod controller; +mod dto; +mod entity; mod repository; mod service; -mod state; mod types; diff --git a/link-for-later/src/repository.rs b/link-for-later/src/repository.rs index 008673a..df2010c 100644 --- a/link-for-later/src/repository.rs +++ b/link-for-later/src/repository.rs @@ -4,10 +4,10 @@ use axum::async_trait; #[cfg(test)] use mockall::{automock, predicate::*}; -use crate::types::{ +use crate::{ dto::{LinkQuery, UserQuery}, entity::{LinkItem, UserInfo}, - Result, + types::Result, }; pub type DynLinks = Arc; diff --git a/link-for-later/src/repository/inmemory.rs b/link-for-later/src/repository/inmemory.rs index b47ec0e..812ace8 100644 --- a/link-for-later/src/repository/inmemory.rs +++ b/link-for-later/src/repository/inmemory.rs @@ -2,10 +2,10 @@ use std::sync::Mutex; use axum::async_trait; -use crate::types::{ +use crate::{ dto::{LinkQuery, LinkQueryBuilder, UserQuery}, entity::{LinkItem, LinkItemBuilder, UserInfo, UserInfoBuilder}, - AppError, Result, + types::{AppError, Result}, }; use super::{Links as LinksRepository, Users as UsersRepository}; @@ -125,7 +125,7 @@ impl UsersRepository for UsersRepositoryProvider { #[cfg(test)] mod tests { - use crate::types::dto::UserQueryBuilder; + use crate::dto::UserQueryBuilder; use super::*; diff --git a/link-for-later/src/repository/mongodb.rs b/link-for-later/src/repository/mongodb.rs index f92a829..2a61472 100644 --- a/link-for-later/src/repository/mongodb.rs +++ b/link-for-later/src/repository/mongodb.rs @@ -3,11 +3,10 @@ use bson::{doc, to_document}; use futures::TryStreamExt; use mongodb::{options::ReplaceOptions, Collection, Database}; -use crate::types::{ +use crate::{ dto::{LinkQuery, LinkQueryBuilder, UserQuery}, - entity::LinkItem, - entity::{LinkItemBuilder, UserInfo, UserInfoBuilder}, - AppError, Result, + entity::{LinkItem, LinkItemBuilder, UserInfo, UserInfoBuilder}, + types::{AppError, Result}, }; use super::{Links as LinksRepository, Users as UsersRepository}; @@ -48,23 +47,23 @@ impl UsersRepositoryProvider { impl LinksRepository for LinksRepositoryProvider { async fn find(&self, query: &LinkQuery) -> Result> { let db_query = - to_document(query).map_err(|_| AppError::DatabaseError("to_document failed".into()))?; + to_document(query).map_err(|_| AppError::Database("to_document failed".into()))?; let result = self .links_collection .find(db_query, None) .await - .map_err(|e| AppError::DatabaseError(format!("find() {e:?}")))?; + .map_err(|e| AppError::Database(format!("find() {e:?}")))?; Ok(result.try_collect().await.unwrap_or_else(|_| vec![])) } async fn get(&self, query: &LinkQuery) -> Result { let db_query = - to_document(query).map_err(|_| AppError::DatabaseError("to_document failed".into()))?; + to_document(query).map_err(|_| AppError::Database("to_document failed".into()))?; let item = self .links_collection .find_one(db_query, None) .await - .map_err(|e| AppError::DatabaseError(format!("find_one() {e:?}")))?; + .map_err(|e| AppError::Database(format!("find_one() {e:?}")))?; item.ok_or_else(|| AppError::LinkNotFound(query.id().to_owned())) } @@ -73,10 +72,10 @@ impl LinksRepository for LinksRepositoryProvider { .links_collection .insert_one(item, None) .await - .map_err(|e| AppError::DatabaseError(format!("insert_one() {e:?}")))?; + .map_err(|e| AppError::Database(format!("insert_one() {e:?}")))?; let id = result.inserted_id.as_object_id().map_or_else( - || Err(AppError::DatabaseError("unexpected inserted_id()".into())), + || Err(AppError::Database("unexpected inserted_id()".into())), |id| Ok(id.to_hex()), )?; let query = doc! {"_id": result.inserted_id}; @@ -84,31 +83,31 @@ impl LinksRepository for LinksRepositoryProvider { self.links_collection .update_one(query, update, None) .await - .map_err(|e| AppError::DatabaseError(format!("update_one() {e:?}")))?; + .map_err(|e| AppError::Database(format!("update_one() {e:?}")))?; Ok(LinkItemBuilder::from(item.clone()).id(&id).build()) } async fn update(&self, id: &str, item: &LinkItem) -> Result { let query = LinkQueryBuilder::new(id, item.owner()).build(); - let db_query = to_document(&query) - .map_err(|_| AppError::DatabaseError("to_document failed".into()))?; + let db_query = + to_document(&query).map_err(|_| AppError::Database("to_document failed".into()))?; let opts = ReplaceOptions::builder().upsert(true).build(); self.links_collection .replace_one(db_query, item, Some(opts)) .await - .map_err(|e| AppError::DatabaseError(format!("replace_one() {e:?}")))?; + .map_err(|e| AppError::Database(format!("replace_one() {e:?}")))?; Ok(item.clone()) } async fn delete(&self, item: &LinkItem) -> Result<()> { let query = LinkQueryBuilder::new(item.id(), item.owner()).build(); - let db_query = to_document(&query) - .map_err(|_| AppError::DatabaseError("to_document failed".into()))?; + let db_query = + to_document(&query).map_err(|_| AppError::Database("to_document failed".into()))?; self.links_collection .delete_one(db_query, None) .await - .map_err(|e| AppError::DatabaseError(format!("delete_one() {e:?}")))?; + .map_err(|e| AppError::Database(format!("delete_one() {e:?}")))?; Ok(()) } } @@ -117,12 +116,12 @@ impl LinksRepository for LinksRepositoryProvider { impl UsersRepository for UsersRepositoryProvider { async fn get(&self, query: &UserQuery) -> Result { let db_query = - to_document(query).map_err(|_| AppError::DatabaseError("to_document failed".into()))?; + to_document(query).map_err(|_| AppError::Database("to_document failed".into()))?; let item = self .users_collection .find_one(db_query, None) .await - .map_err(|e| AppError::DatabaseError(format!("find_one() {e:?}")))?; + .map_err(|e| AppError::Database(format!("find_one() {e:?}")))?; item.ok_or_else(|| AppError::UserNotFound(query.email().to_owned())) } @@ -131,10 +130,10 @@ impl UsersRepository for UsersRepositoryProvider { .users_collection .insert_one(info, None) .await - .map_err(|e| AppError::DatabaseError(format!("insert_one() {e:?}")))?; + .map_err(|e| AppError::Database(format!("insert_one() {e:?}")))?; let id = result.inserted_id.as_object_id().map_or_else( - || Err(AppError::DatabaseError("unexpected inserted_id()".into())), + || Err(AppError::Database("unexpected inserted_id()".into())), |id| Ok(id.to_hex()), )?; let query = doc! {"_id": result.inserted_id}; @@ -142,7 +141,7 @@ impl UsersRepository for UsersRepositoryProvider { self.users_collection .update_one(query, update, None) .await - .map_err(|e| AppError::DatabaseError(format!("update_one() {e:?}")))?; + .map_err(|e| AppError::Database(format!("update_one() {e:?}")))?; Ok(UserInfoBuilder::from(info.clone()).id(&id).build()) } diff --git a/link-for-later/src/service.rs b/link-for-later/src/service.rs index 22d76c3..6291701 100644 --- a/link-for-later/src/service.rs +++ b/link-for-later/src/service.rs @@ -5,13 +5,10 @@ use axum::async_trait; use mockall::{automock, predicate::*}; use crate::{ + dto::{LinkQuery, Token}, + entity::{LinkItem, UserInfo}, repository, - types::{ - auth::Token, - dto::LinkQuery, - entity::{LinkItem, UserInfo}, - Result, - }, + types::Result, }; pub type DynLinks = Arc; diff --git a/link-for-later/src/service/links.rs b/link-for-later/src/service/links.rs index d5c636b..ae3f51d 100644 --- a/link-for-later/src/service/links.rs +++ b/link-for-later/src/service/links.rs @@ -2,13 +2,11 @@ use axum::async_trait; use chrono::Utc; use crate::{ + dto::{LinkQuery, LinkQueryBuilder}, + entity::{LinkItem, LinkItemBuilder}, repository, service::Links as LinksService, - types::{ - dto::{LinkQuery, LinkQueryBuilder}, - entity::{LinkItem, LinkItemBuilder}, - Result, - }, + types::Result, }; pub struct ServiceProvider {} @@ -147,14 +145,14 @@ mod tests { .expect_find() .withf(move |query| query == &expected_query) .times(1) - .returning(|_| Err(AppError::TestError)); + .returning(|_| Err(AppError::Test)); let links_service = ServiceProvider {}; let response = links_service .search(Box::new(Arc::new(mock_links_repo)), &repo_query) .await; - assert_eq!(response, Err(AppError::TestError)); + assert_eq!(response, Err(AppError::Test)); } #[tokio::test] @@ -239,14 +237,14 @@ mod tests { .expect_create() //.withf(move |item| item == &item_to_create) .times(1) - .returning(|_| Err(AppError::TestError)); + .returning(|_| Err(AppError::Test)); let links_service = ServiceProvider {}; let response = links_service .create(Box::new(Arc::new(mock_links_repo)), &request_item) .await; - assert_eq!(response, Err(AppError::TestError)); + assert_eq!(response, Err(AppError::Test)); } #[tokio::test] @@ -348,14 +346,14 @@ mod tests { //.withf(move |item| item == &item_to_update) .times(1) .in_sequence(&mut seq) - .returning(|_, _| Err(AppError::TestError)); + .returning(|_, _| Err(AppError::Test)); let links_service = ServiceProvider {}; let response = links_service .update(Box::new(Arc::new(mock_links_repo)), "1", &request_item) .await; - assert_eq!(response, Err(AppError::TestError)); + assert_eq!(response, Err(AppError::Test)); } #[tokio::test] @@ -450,13 +448,13 @@ mod tests { //.withf(move |item| item == &item_to_delete) .times(1) .in_sequence(&mut seq) - .returning(|_| Err(AppError::TestError)); + .returning(|_| Err(AppError::Test)); let links_service = ServiceProvider {}; let response = links_service .delete(Box::new(Arc::new(mock_links_repo)), &request_item) .await; - assert_eq!(response, Err(AppError::TestError)); + assert_eq!(response, Err(AppError::Test)); } } diff --git a/link-for-later/src/service/users.rs b/link-for-later/src/service/users.rs index 71f399c..a851374 100644 --- a/link-for-later/src/service/users.rs +++ b/link-for-later/src/service/users.rs @@ -8,14 +8,11 @@ use jsonwebtoken::{encode, EncodingKey, Header}; use std::convert::TryInto; use crate::{ + dto::{Claims, Token, UserQueryBuilder}, + entity::{UserInfo, UserInfoBuilder}, repository, service::Users as UsersService, - types::{ - auth::{Claims, Token}, - dto::UserQueryBuilder, - entity::{UserInfo, UserInfoBuilder}, - AppError, Result, - }, + types::{AppError, Result}, }; const JWT_SECRET_KEY: &str = "JWT_SECRET"; @@ -43,7 +40,7 @@ impl UsersService for ServiceProvider { user_info.password().as_bytes(), &SaltString::generate(&mut OsRng), ) - .map_err(|e| AppError::ServerError(format!("hash_password() {e:?}")))? + .map_err(|e| AppError::Server(format!("hash_password() {e:?}")))? .to_string(); let registered_user_info = UserInfoBuilder::new(user_info.email(), &password_hash) @@ -64,7 +61,7 @@ impl UsersService for ServiceProvider { let retrieved_user_info = users_repo.get(&user_query).await?; let parsed_hash = PasswordHash::new(retrieved_user_info.password()) - .map_err(|e| AppError::ServerError(format!("PasswordHash::new() {e:?}")))?; + .map_err(|e| AppError::Server(format!("PasswordHash::new() {e:?}")))?; Argon2::default() .verify_password(user_info.password().as_bytes(), &parsed_hash) .map_err(|_| AppError::IncorrectPassword(user_info.email().to_owned()))?; @@ -73,7 +70,7 @@ impl UsersService for ServiceProvider { let timestamp: usize = timestamp .timestamp() .try_into() - .map_err(|e| AppError::ServerError(format!("timestamp() {e:?}")))?; + .map_err(|e| AppError::Server(format!("timestamp() {e:?}")))?; Ok(timestamp) }; @@ -91,7 +88,7 @@ impl UsersService for ServiceProvider { &claims, &EncodingKey::from_secret(secret.as_bytes()), ) - .map_err(|e| AppError::ServerError(format!("encode() {e:?}")))?; + .map_err(|e| AppError::Server(format!("encode() {e:?}")))?; Ok(Token::new(&token)) } @@ -172,7 +169,7 @@ mod tests { .expect_get() .withf(move |query| query == &repo_query) .times(1) - .returning(|_| Err(AppError::TestError)); + .returning(|_| Err(AppError::Test)); mock_users_repo.expect_create().times(0); let users_service = ServiceProvider {}; @@ -180,7 +177,7 @@ mod tests { .register(Box::new(Arc::new(mock_users_repo)), &request_item) .await; - assert_eq!(response, Err(AppError::TestError)); + assert_eq!(response, Err(AppError::Test)); } #[tokio::test] @@ -199,14 +196,14 @@ mod tests { .expect_create() //.withf(move |user| user == &user_to_register) .times(1) - .returning(move |_| Err(AppError::TestError)); + .returning(move |_| Err(AppError::Test)); let users_service = ServiceProvider {}; let response = users_service .register(Box::new(Arc::new(mock_users_repo)), &request_item) .await; - assert_eq!(response, Err(AppError::TestError)); + assert_eq!(response, Err(AppError::Test)); } #[tokio::test] diff --git a/link-for-later/src/state.rs b/link-for-later/src/state.rs deleted file mode 100644 index 057302d..0000000 --- a/link-for-later/src/state.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::{ - repository::{DynLinks as DynLinksRepository, DynUsers as DynUsersRepository}, - service::{DynLinks as DynLinksService, DynUsers as DynUsersService}, -}; - -#[allow(clippy::module_name_repetitions)] -#[derive(Clone)] -pub struct AppState { - links_service: DynLinksService, - users_service: DynUsersService, - links_repo: DynLinksRepository, - users_repo: DynUsersRepository, -} - -impl AppState { - pub fn new( - links_service: DynLinksService, - users_service: DynUsersService, - links_repo: DynLinksRepository, - users_repo: DynUsersRepository, - ) -> Self { - Self { - links_service, - users_service, - links_repo, - users_repo, - } - } - - pub fn links_service(&self) -> &DynLinksService { - &self.links_service - } - - pub fn users_service(&self) -> &DynUsersService { - &self.users_service - } - - pub fn links_repo(&self) -> &DynLinksRepository { - &self.links_repo - } - - pub fn users_repo(&self) -> &DynUsersRepository { - &self.users_repo - } -} diff --git a/link-for-later/src/types.rs b/link-for-later/src/types.rs index 4133d87..de41248 100644 --- a/link-for-later/src/types.rs +++ b/link-for-later/src/types.rs @@ -1,13 +1,9 @@ -pub use self::error::App as AppError; - pub type Result = std::result::Result; +pub type AppState = crate::app::State; +pub type AppError = crate::app::Error; + pub enum Database { MongoDb(mongodb::Database), InMemory, } - -pub mod auth; -pub mod dto; -pub mod entity; -pub mod error; diff --git a/link-for-later/src/types/dto.rs b/link-for-later/src/types/dto.rs deleted file mode 100644 index 1a586db..0000000 --- a/link-for-later/src/types/dto.rs +++ /dev/null @@ -1,186 +0,0 @@ -use serde::{Deserialize, Serialize}; -use validator::Validate; - -#[derive(Default, Serialize, Deserialize, Validate)] -pub struct LinkItemRequest { - #[validate(url)] - url: String, - #[serde(default = "String::default")] - title: String, - #[serde(default = "String::default")] - description: String, -} - -impl LinkItemRequest { - pub fn url(&self) -> &str { - &self.url - } - - pub fn title(&self) -> &str { - &self.title - } - - pub fn description(&self) -> &str { - &self.description - } - - #[cfg(test)] - pub fn new(url: &str) -> Self { - Self { - url: url.to_string(), - ..Default::default() - } - } -} - -#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct LinkQuery { - #[serde(skip_serializing_if = "String::is_empty")] - id: String, - #[serde(skip_serializing_if = "String::is_empty")] - owner: String, -} - -impl LinkQuery { - pub fn id(&self) -> &str { - &self.id - } - - pub fn owner(&self) -> &str { - &self.owner - } -} - -#[derive(Default)] -pub struct LinkQueryBuilder { - id: String, - owner: String, -} - -impl LinkQueryBuilder { - pub fn new(id: &str, owner: &str) -> Self { - Self { - id: id.to_string(), - owner: owner.to_string(), - } - } - - pub fn owner(mut self, owner: &str) -> Self { - self.owner = owner.to_string(); - self - } - - pub fn build(self) -> LinkQuery { - LinkQuery { - id: self.id, - owner: self.owner, - } - } -} - -#[derive(Serialize, Deserialize, Validate)] -pub struct UserInfoRequest { - #[validate(email)] - email: String, - password: String, -} - -impl UserInfoRequest { - pub fn email(&self) -> &str { - &self.email - } - - pub fn password(&self) -> &str { - &self.password - } - - #[cfg(test)] - pub fn new(email: &str, password: &str) -> Self { - Self { - email: email.to_string(), - password: password.to_string(), - } - } -} - -#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct UserQuery { - email: String, -} - -impl UserQuery { - pub fn email(&self) -> &str { - &self.email - } -} - -#[derive(Default)] -pub struct UserQueryBuilder { - email: String, -} - -impl UserQueryBuilder { - pub fn new(email: &str) -> Self { - Self { - email: email.to_string(), - } - } - - pub fn build(self) -> UserQuery { - UserQuery { email: self.email } - } -} - -#[derive(Serialize, Deserialize)] -pub struct AuthResponse { - token: String, -} - -impl AuthResponse { - pub fn new(token: &str) -> Self { - Self { - token: token.to_string(), - } - } -} - -/* -#[cfg(test)] -pub mod tests { - use super::LinkItemRequest; - - #[derive(Default)] - pub struct LinkItemRequestBuilder { - url: String, - title: String, - description: String, - } - - impl LinkItemRequestBuilder { - pub fn new(url: &str) -> Self { - Self { - url: url.to_string(), - ..Default::default() - } - } - - pub fn title(mut self, title: &str) -> Self { - self.title = title.to_string(); - self - } - - pub fn description(mut self, description: &str) -> Self { - self.description = description.to_string(); - self - } - - pub fn build(self) -> LinkItemRequest { - LinkItemRequest { - url: self.url, - title: self.title, - description: self.description, - } - } - } -} -*/ diff --git a/link-for-later/src/types/error.rs b/link-for-later/src/types/error.rs deleted file mode 100644 index 0ac2723..0000000 --- a/link-for-later/src/types/error.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::{error, fmt}; - -#[derive(Debug, PartialEq, Eq)] -pub enum App { - ServerError(String), - DatabaseError(String), - LinkNotFound(String), - UserAlreadyExists(String), - UserNotFound(String), - IncorrectPassword(String), - AuthorizationError(String), - ValidationError(String), - - #[cfg(test)] - TestError, -} - -impl fmt::Display for App { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Self::ServerError(_) => write!(f, "server error"), - Self::DatabaseError(_) => write!(f, "database error"), - Self::LinkNotFound(_) => write!(f, "link item not found"), - Self::UserAlreadyExists(_) => write!(f, "user already registered"), - Self::UserNotFound(_) => write!(f, "user not found"), - Self::IncorrectPassword(_) => write!(f, "incorrect password for user"), - Self::AuthorizationError(_) => write!(f, "invalid authorization token"), - Self::ValidationError(_) => write!(f, "invalid request"), - - #[cfg(test)] - Self::TestError => write!(f, "test error"), - } - } -} - -impl error::Error for App {} - -#[cfg(test)] -mod tests { - - use super::App as AppError; - - #[test] - fn test_error_messages() { - assert_eq!( - AppError::ServerError("a server operation failed".into()).to_string(), - "server error" - ); - assert_eq!( - AppError::DatabaseError("a database operation failed".into()).to_string(), - "database error" - ); - assert_eq!( - AppError::LinkNotFound("link".into()).to_string(), - "link item not found" - ); - assert_eq!( - AppError::UserAlreadyExists("user".into()).to_string(), - "user already registered" - ); - assert_eq!( - AppError::UserNotFound("user".into()).to_string(), - "user not found" - ); - assert_eq!( - AppError::IncorrectPassword("user".into()).to_string(), - "incorrect password for user" - ); - assert_eq!( - AppError::AuthorizationError("an authorization error occurred".into()).to_string(), - "invalid authorization token" - ); - assert_eq!( - AppError::ValidationError("invalid email".into()).to_string(), - "invalid request" - ); - } -}