commit bbce466a0ea861eea33b8dcbe88f2bf0fd539a36 Author: Aravinth Manivannan Date: Tue Apr 23 14:10:05 2024 +0530 dump diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e53951d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,266 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + +[[package]] +name = "serde_json" +version = "1.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "todos" +version = "0.1.0" +dependencies = [ + "derive_more", + "serde", + "validator", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "validator" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db79c75af171630a3148bd3e6d7c4f42b6a9a014c2945bc5ed0020cbb8d9478e" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..622c2ff --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "todos" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +derive_more = "0.99.17" +serde = { version = "1.0.197", features = ["derive"] } +validator = "0.18.1" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..df592e5 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,7 @@ +//#![allow(dead_code)] + +//mod project; +mod user; +fn main() { + println!("Hello, world!"); +} diff --git a/src/project.rs b/src/project.rs new file mode 100644 index 0000000..16229ea --- /dev/null +++ b/src/project.rs @@ -0,0 +1,116 @@ +use crate::user::User; +use derive_more::{Display, Error}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct Project { + id: usize, + name: String, + description: Option, + members: Vec, +} + +pub mod services { + use super::*; + + pub fn get_projects_belonging_to_user( + project_adapter: &dyn SecondaryPortProject, + owner: &User, + ) -> ProjectResult> { + project_adapter.get_projects_belonging_to_user(owner.id()) + } + + pub fn create( + project_adapter: &dyn SecondaryPortProject, + owner: User, + name: String, + description: Option, + ) -> ProjectResult { + let id = project_adapter.create_project(&name, description.as_deref(), owner.id())?; + Ok(Project::new(id, name, description, vec![owner])) + } + + fn doer_is_member( + project_adapter: &dyn SecondaryPortProject, + project_id: usize, + doer: &User, + ) -> ProjectResult { + let p = project_adapter.get_project_by_id(project_id)?; + Ok(p.doer_is_member(doer)) + } + pub fn update_description( + project_adapter: &dyn SecondaryPortProject, + project_id: usize, + doer: &User, + new_description: String, + ) -> ProjectResult { + if doer_is_member(project_adapter, project_id, doer)? { + let mut p = project_adapter.get_project_by_id(project_id)?; + project_adapter.update_description(p.id, &new_description)?; + p.update_description(new_description); + Ok(p) + } else { + Err(ProjectError::Unauthorized) + } + } +} + +impl Project { + // getters + pub fn id(&self) -> usize { + self.id + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn description(&self) -> Option<&str> { + self.description.as_deref() + } + pub fn email(&self) -> &[User] { + &self.members + } + + fn new(id: usize, name: String, description: Option, members: Vec) -> Self { + Self { + id, + name, + description, + members, + } + } + fn update_description(&mut self, new_description: String) { + self.description = Some(new_description) + } + + fn doer_is_member(&self, doer: &User) -> bool { + self.members.contains(doer) + } + + pub fn load(id: usize, name: String, description: Option, members: Vec) -> Self { + Self::new(id, name, description, members) + } +} + +pub type ProjectResult = Result; + +#[derive(Debug, Display, Error)] +pub enum ProjectError { + Unauthorized, +} + +pub trait SecondaryPortProject { + // returns ID of newly created Project + fn create_project( + &self, + name: &str, + description: Option<&str>, + owner_id: usize, + ) -> ProjectResult; + fn update_description(&self, id: usize, new_description: &str) -> ProjectResult; + + // get user from DB + fn get_project_by_id(&self, id: usize) -> ProjectResult; + fn get_projects_belonging_to_user(&self, user_id: usize) -> ProjectResult>; +} diff --git a/src/user/adapter/in/mod.rs b/src/user/adapter/in/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/user/adapter/mod.rs b/src/user/adapter/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/user/adapter/mod.rs @@ -0,0 +1 @@ + diff --git a/src/user/adapter/out/mod.rs b/src/user/adapter/out/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/user/application/fetch_user_service/command.rs b/src/user/application/fetch_user_service/command.rs new file mode 100644 index 0000000..e610d2e --- /dev/null +++ b/src/user/application/fetch_user_service/command.rs @@ -0,0 +1,37 @@ +use serde::{Deserialize, Serialize}; + +use crate::user::domain::UserError; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct FetchUserCommand<'a> { + login: &'a str, +} + +impl<'a> FetchUserCommand<'a> { + pub fn login(&self) -> &str { + &self.login + } + + pub fn new_command(login: &'a str) -> Result { + let login = login.trim(); + if login.trim().is_empty() { + Err(UserError::LoginIsEmpty) + } else { + Ok(Self { login }) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fetch_user_cmd() { + assert_eq!( + FetchUserCommand::new_command(" ",).err(), + Some(UserError::LoginIsEmpty) + ); + assert!(FetchUserCommand::new_command("aasdad",).is_ok()) + } +} diff --git a/src/user/application/fetch_user_service/mod.rs b/src/user/application/fetch_user_service/mod.rs new file mode 100644 index 0000000..ad250db --- /dev/null +++ b/src/user/application/fetch_user_service/mod.rs @@ -0,0 +1,2 @@ +pub mod command; +pub mod service; diff --git a/src/user/application/fetch_user_service/service.rs b/src/user/application/fetch_user_service/service.rs new file mode 100644 index 0000000..8ae4b53 --- /dev/null +++ b/src/user/application/fetch_user_service/service.rs @@ -0,0 +1,58 @@ +use super::command; +use crate::user::application::{port::out::get_user, utils}; +use crate::user::domain::{User, UserResult}; + +use get_user::GetUserPort; + +pub trait FetchUserTrait { + fn fetch_user_from_login(&self, cmd: command::FetchUserCommand) -> UserResult; +} + +pub struct FetchUserService { + get_user: Box, +} + +impl FetchUserService { + pub fn new(get_user: Box) -> Self { + Self { get_user } + } +} + +impl FetchUserTrait for FetchUserService { + fn fetch_user_from_login(&self, cmd: command::FetchUserCommand) -> UserResult { + utils::fetch_user_from_login(self.get_user.as_ref(), cmd.login()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::user::domain::tests::utils::*; + + pub mod utils { + use super::*; + use crate::user::application::port::out::tests::utils::MockOutPortAdapter; + + impl FetchUserService { + pub fn get_fetch_user_service() -> Self { + FetchUserService::new(Box::new(MockOutPortAdapter::new())) + } + } + } + + #[test] + fn test_fetch_user_from_login() { + let u = get_user(); + let s = FetchUserService::get_fetch_user_service(); + assert_eq!( + s.fetch_user_from_login(command::FetchUserCommand::new_command(u.name()).unwrap()) + .unwrap(), + u + ); + assert_eq!( + s.fetch_user_from_login(command::FetchUserCommand::new_command(u.email()).unwrap()) + .unwrap(), + u + ); + } +} diff --git a/src/user/application/mod.rs b/src/user/application/mod.rs new file mode 100644 index 0000000..cbfd8a4 --- /dev/null +++ b/src/user/application/mod.rs @@ -0,0 +1,6 @@ +mod fetch_user_service; +mod port; +mod register_service; +mod signin_service; +mod update_user_password_service; +mod utils; diff --git a/src/user/application/port/mod.rs b/src/user/application/port/mod.rs new file mode 100644 index 0000000..9995c3d --- /dev/null +++ b/src/user/application/port/mod.rs @@ -0,0 +1 @@ +pub mod out; diff --git a/src/user/application/port/out/create_user.rs b/src/user/application/port/out/create_user.rs new file mode 100644 index 0000000..daec407 --- /dev/null +++ b/src/user/application/port/out/create_user.rs @@ -0,0 +1,6 @@ +use crate::user::domain::UserResult; + +pub trait CreateUserPort { + // returns ID of newly created user user + fn create_user(&self, name: &str, password: &str, email: &str) -> UserResult<()>; +} diff --git a/src/user/application/port/out/get_user.rs b/src/user/application/port/out/get_user.rs new file mode 100644 index 0000000..13ad1a3 --- /dev/null +++ b/src/user/application/port/out/get_user.rs @@ -0,0 +1,8 @@ +use crate::user::domain::{User, UserResult}; + +pub trait GetUserPort { + // get user from DB + fn get_user_by_name(&self, name: &str) -> UserResult; + // get user from DB + fn get_user_by_email(&self, email: &str) -> UserResult; +} diff --git a/src/user/application/port/out/mod.rs b/src/user/application/port/out/mod.rs new file mode 100644 index 0000000..4bb652d --- /dev/null +++ b/src/user/application/port/out/mod.rs @@ -0,0 +1,84 @@ +pub(in crate::user::application) mod create_user; +pub(in crate::user::application) mod get_user; +pub(in crate::user::application) mod update_password; + +#[cfg(test)] +pub(crate) mod tests { + pub(crate) mod utils { + use std::sync::{Arc, RwLock}; + + use crate::user::application::port::out::{ + create_user::CreateUserPort, get_user::GetUserPort, update_password::UpdatePasswordPort, + }; + use crate::user::domain::{tests::utils::*, User, UserResult}; + + #[derive(Clone)] + pub struct MockOutPortAdapter { + users: Arc>>, + } + + impl MockOutPortAdapter { + pub fn new() -> Self { + Self { + users: Arc::new(RwLock::new(Vec::default())), + } + } + + pub fn get_user(&self, name: &str) -> User { + let users = self.users.read().unwrap(); + + users + .get(users.iter().position(|u| u.name() == name).unwrap()) + .unwrap() + .to_owned() + } + } + + impl GetUserPort for MockOutPortAdapter { + fn get_user_by_name(&self, name: &str) -> UserResult { + let mut users = self.users.write().unwrap(); + if let Some(pos) = users.iter().position(|u| u.name() == name) { + Ok(users.get(pos).unwrap().to_owned()) + } else { + let u = get_user_by_name(name.to_string()); + users.push(u.clone()); + Ok(u) + } + } + + fn get_user_by_email(&self, email: &str) -> UserResult { + let mut users = self.users.write().unwrap(); + if let Some(pos) = users.iter().position(|u| u.email() == email) { + Ok(users.get(pos).unwrap().to_owned()) + } else { + let u = get_user_by_email(email.to_string()); + users.push(u.clone()); + Ok(u) + } + } + } + + impl UpdatePasswordPort for MockOutPortAdapter { + fn update_password(&self, id: usize, new_password: &str) -> UserResult<()> { + let mut users = self.users.write().unwrap(); + if let Some(pos) = users.iter().position(|u| u.id() == id) { + let u = users.get_mut(pos).unwrap(); + u.set_password(new_password.to_owned()); + } else { + let mut u = get_user_by_id(id); + u.set_password(new_password.to_owned()); + users.push(u); + }; + Ok(()) + } + } + + impl CreateUserPort for MockOutPortAdapter { + fn create_user(&self, name: &str, password: &str, email: &str) -> UserResult<()> { + let u = User::new(1, name.into(), password.into(), email.into()); + self.users.write().unwrap().push(u); + Ok(()) + } + } + } +} diff --git a/src/user/application/port/out/update_password.rs b/src/user/application/port/out/update_password.rs new file mode 100644 index 0000000..6f0be01 --- /dev/null +++ b/src/user/application/port/out/update_password.rs @@ -0,0 +1,5 @@ +use crate::user::domain::UserResult; + +pub trait UpdatePasswordPort { + fn update_password(&self, id: usize, new_password: &str) -> UserResult<()>; +} diff --git a/src/user/application/register_service/command.rs b/src/user/application/register_service/command.rs new file mode 100644 index 0000000..2f084b6 --- /dev/null +++ b/src/user/application/register_service/command.rs @@ -0,0 +1,114 @@ +use serde::{Deserialize, Serialize}; +use validator::ValidateEmail; + +use crate::user::domain::UserError; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct RegisterUserCommand { + name: String, + password: String, + email: String, +} + +impl RegisterUserCommand { + pub fn name(&self) -> &str { + &self.name + } + + pub fn password(&self) -> &str { + &self.password + } + pub fn email(&self) -> &str { + &self.email + } + + pub fn new_command( + name: String, + password: String, + confirm_password: String, + email: String, + ) -> Result { + let name = name.trim().to_owned(); + let email = email.trim().to_owned(); + + if name.is_empty() { + return Err(UserError::UsernameIsEmpty); + } + + if confirm_password != password { + return Err(UserError::PasswordsDontMatch); + } + + if !email.validate_email() { + return Err(UserError::NotAnEmail); + } + + Ok(Self { + name, + password, + email, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::user::domain::tests::utils::*; + + #[test] + fn test_register_cmd() { + let u = get_user(); + + assert_eq!( + RegisterUserCommand::new_command( + " ".to_string(), + u.password().to_string(), + u.password().to_string(), + u.email().to_string(), + ) + .err(), + Some(UserError::UsernameIsEmpty) + ); + + assert_eq!( + RegisterUserCommand::new_command( + u.name().to_string(), + u.password().to_string(), + u.name().to_string(), + u.email().to_string(), + ) + .err(), + Some(UserError::PasswordsDontMatch) + ); + + assert_eq!( + RegisterUserCommand::new_command( + u.name().to_string(), + u.password().to_string(), + u.password().to_string(), + u.password().to_string(), + ) + .err(), + Some(UserError::NotAnEmail) + ); + + let _ = RegisterUserCommand::get_command(); + } + + pub(crate) mod utils { + use super::*; + impl RegisterUserCommand { + pub(crate) fn get_command() -> Self { + let u = get_user(); + RegisterUserCommand::new_command( + u.name().to_string(), + u.password().to_string(), + u.password().to_string(), + u.email().to_string(), + ) + .unwrap() + } + } + } +} diff --git a/src/user/application/register_service/mod.rs b/src/user/application/register_service/mod.rs new file mode 100644 index 0000000..ad250db --- /dev/null +++ b/src/user/application/register_service/mod.rs @@ -0,0 +1,2 @@ +pub mod command; +pub mod service; diff --git a/src/user/application/register_service/service.rs b/src/user/application/register_service/service.rs new file mode 100644 index 0000000..116f765 --- /dev/null +++ b/src/user/application/register_service/service.rs @@ -0,0 +1,63 @@ +use crate::user::application::port::out::create_user; +use crate::user::domain::UserResult; + +use super::command; + +pub trait RegisterUserTrait { + fn register(&self, cmd: command::RegisterUserCommand) -> UserResult<()>; +} + +pub struct RegisterUserService { + crate_user_adapter: Box, +} + +impl RegisterUserService { + pub fn new(crate_user_adapter: Box) -> Self { + Self { crate_user_adapter } + } +} + +impl RegisterUserTrait for RegisterUserService { + fn register(&self, cmd: command::RegisterUserCommand) -> UserResult<()> { + self.crate_user_adapter + .create_user(cmd.name(), cmd.password(), cmd.email())?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use tests::command::RegisterUserCommand; + + use super::*; + use crate::user::domain::tests::utils::*; + + pub mod utils { + use super::*; + use crate::user::application::port::out::tests::utils::MockOutPortAdapter; + + impl RegisterUserService { + pub fn get_register_service() -> (Self, MockOutPortAdapter) { + let adapter = MockOutPortAdapter::new(); + (RegisterUserService::new(Box::new(adapter.clone())), adapter) + } + } + } + + #[test] + fn test_register_service() { + let u = get_user(); + + let (s, adapter) = RegisterUserService::get_register_service(); + let cmd = RegisterUserCommand::get_command(); + + s.register(cmd).unwrap(); + let new_user = adapter.get_user(u.name()); + assert_eq!(new_user, u); + assert_ne!( + new_user.id(), + 0, + "default ID used to create user before getting ID assigned by DB" + ) + } +} diff --git a/src/user/application/signin_service/command.rs b/src/user/application/signin_service/command.rs new file mode 100644 index 0000000..4dbd0e9 --- /dev/null +++ b/src/user/application/signin_service/command.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; + +use crate::user::domain::UserError; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct SigninUserCommand<'a> { + login: &'a str, + password: &'a str, +} + +impl<'a> SigninUserCommand<'a> { + pub fn password(&self) -> &str { + &self.password + } + + pub fn login(&self) -> &str { + &self.login + } + + pub fn new_command(login: &'a str, password: &'a str) -> Result { + let login = login.trim(); + if password.is_empty() { + Err(UserError::PasswordIsEmpty) + } else if login.is_empty() { + Err(UserError::LoginIsEmpty) + } else { + Ok(Self { password, login }) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sign_user_cmd() { + assert_eq!( + SigninUserCommand::new_command("foo", "",).err(), + Some(UserError::PasswordIsEmpty) + ); + + assert_eq!( + SigninUserCommand::new_command(" ", "foo",).err(), + Some(UserError::LoginIsEmpty) + ); + assert!(SigninUserCommand::new_command("foo", "foo",).is_ok(),); + } +} diff --git a/src/user/application/signin_service/mod.rs b/src/user/application/signin_service/mod.rs new file mode 100644 index 0000000..ad250db --- /dev/null +++ b/src/user/application/signin_service/mod.rs @@ -0,0 +1,2 @@ +pub mod command; +pub mod service; diff --git a/src/user/application/signin_service/service.rs b/src/user/application/signin_service/service.rs new file mode 100644 index 0000000..8efb495 --- /dev/null +++ b/src/user/application/signin_service/service.rs @@ -0,0 +1,74 @@ +use super::command; +use crate::user::application::{port::out::get_user, utils}; +use crate::user::domain::{UserError, UserResult}; +use get_user::GetUserPort; + +pub trait SigninUserTrait { + fn signin(&self, cmd: command::SigninUserCommand) -> UserResult<()>; +} + +pub struct SigninUserService { + get_user: Box, +} + +impl SigninUserService { + pub fn new(get_user: Box) -> Self { + Self { get_user } + } +} + +impl SigninUserTrait for SigninUserService { + fn signin(&self, cmd: command::SigninUserCommand) -> UserResult<()> { + let u = utils::fetch_user_from_login(self.get_user.as_ref(), cmd.login())?; + if u.password() == cmd.password() { + Ok(()) + } else { + Err(UserError::WrongPassword) + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::user::domain::tests::utils::*; + use crate::user::domain::UserError; + + pub mod utils { + use crate::user::application::port::out::tests::utils::MockOutPortAdapter; + + use super::*; + impl SigninUserService { + pub fn get_signin_user_service() -> Self { + SigninUserService::new(Box::new(MockOutPortAdapter::new())) + } + } + } + + #[test] + fn test_signin_service() { + let u = get_user(); + let s = SigninUserService::get_signin_user_service(); + + assert_eq!( + s.signin(command::SigninUserCommand::new_command(u.name(), u.name()).unwrap()) + .err(), + Some(UserError::WrongPassword) + ); + + assert_eq!( + s.signin(command::SigninUserCommand::new_command(u.email(), u.name()).unwrap()) + .err(), + Some(UserError::WrongPassword) + ); + + assert!(s + .signin(command::SigninUserCommand::new_command(u.email(), u.password()).unwrap()) + .is_ok()); + + assert!(s + .signin(command::SigninUserCommand::new_command(u.name(), u.password()).unwrap()) + .is_ok()); + } +} diff --git a/src/user/application/update_user_password_service/command.rs b/src/user/application/update_user_password_service/command.rs new file mode 100644 index 0000000..714b7f9 --- /dev/null +++ b/src/user/application/update_user_password_service/command.rs @@ -0,0 +1,77 @@ +use crate::user::domain::UserError; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct UpdateUserPasswordCommand<'a> { + login: &'a str, + old_password: &'a str, + new_password: String, +} + +impl<'a> UpdateUserPasswordCommand<'a> { + pub fn login(&self) -> &str { + &self.login + } + + pub fn new_password(&self) -> &str { + &self.new_password + } + + pub fn old_password(&self) -> &str { + self.old_password + } + + pub fn new_command( + login: &'a str, + old_password: &'a str, + new_password: String, + new_password_confirm: String, + ) -> Result { + let login = login.trim(); + + if login.is_empty() { + return Err(UserError::LoginIsEmpty); + } + + if new_password != new_password_confirm { + return Err(UserError::PasswordsDontMatch); + } + + Ok(Self { + login, + old_password, + new_password, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::user::domain::UserError; + + #[test] + fn test_update_password_command() { + assert_eq!( + UpdateUserPasswordCommand::new_command( + " ", + "password", + "new_password".to_string(), + "new_password".to_string(), + ) + .err(), + Some(UserError::LoginIsEmpty) + ); + + assert_eq!( + UpdateUserPasswordCommand::new_command( + "user", + "password", + "new_password1".to_string(), + "new_password".to_string() + ) + .err(), + Some(UserError::PasswordsDontMatch) + ); + } +} diff --git a/src/user/application/update_user_password_service/mod.rs b/src/user/application/update_user_password_service/mod.rs new file mode 100644 index 0000000..ad250db --- /dev/null +++ b/src/user/application/update_user_password_service/mod.rs @@ -0,0 +1,2 @@ +pub mod command; +pub mod service; diff --git a/src/user/application/update_user_password_service/service.rs b/src/user/application/update_user_password_service/service.rs new file mode 100644 index 0000000..753987b --- /dev/null +++ b/src/user/application/update_user_password_service/service.rs @@ -0,0 +1,114 @@ +use super::command; +use crate::user::application::{ + port::out::{get_user::GetUserPort, update_password::UpdatePasswordPort}, + utils, +}; +use crate::user::domain::{UserError, UserResult}; + +pub trait UpdateUserPasswordTrait { + fn update_password(&self, cmd: command::UpdateUserPasswordCommand) -> UserResult<()>; +} + +pub struct UpdateUserPasswordService { + get_user: Box, + update_password: Box, +} + +impl UpdateUserPasswordService { + pub fn new( + get_user: Box, + update_password: Box, + ) -> Self { + Self { + get_user, + update_password, + } + } +} + +impl UpdateUserPasswordTrait for UpdateUserPasswordService { + fn update_password(&self, cmd: command::UpdateUserPasswordCommand) -> UserResult<()> { + let mut u = utils::fetch_user_from_login(self.get_user.as_ref(), cmd.login())?; + + if u.password() != cmd.old_password() { + return Err(UserError::WrongPassword); + } + + if u.password() == cmd.new_password() { + return Err(UserError::PasswordAlreadyUsed); + } + + u.set_password(cmd.new_password().to_owned()); + self.update_password.update_password(u.id(), u.password())?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::user::domain::tests::utils::*; + use crate::user::domain::UserError; + + pub mod utils { + use crate::user::application::port::out::tests::utils::MockOutPortAdapter; + + use super::*; + impl UpdateUserPasswordService { + pub fn get_update_user_password_service() -> Self { + let a = MockOutPortAdapter::new(); + UpdateUserPasswordService::new(Box::new(a.clone()), Box::new(a.clone())) + } + } + } + + #[test] + fn test_update_password_service() { + let u = get_user(); + let s = UpdateUserPasswordService::get_update_user_password_service(); + let new_passowrd = "newpassword"; + + assert_eq!( + s.update_password( + command::UpdateUserPasswordCommand::new_command( + u.name(), + "wrong_password", + new_passowrd.into(), + new_passowrd.into() + ) + .unwrap() + ) + .err(), + Some(UserError::WrongPassword) + ); + let old_password = u.password().to_owned(); + + assert_eq!( + s.update_password( + command::UpdateUserPasswordCommand::new_command( + u.name(), + &old_password, + old_password.clone(), + old_password.clone(), + ) + .unwrap() + ) + .err(), + Some(UserError::PasswordAlreadyUsed) + ); + s.update_password( + command::UpdateUserPasswordCommand::new_command( + u.name(), + &old_password, + new_passowrd.into(), + new_passowrd.into(), + ) + .unwrap(), + ) + .unwrap(); + let updated_user = + super::utils::fetch_user_from_login(s.get_user.as_ref(), u.name()).unwrap(); + assert_ne!(updated_user.password(), u.password()); + assert_eq!(updated_user.password(), new_passowrd); + } +} diff --git a/src/user/application/utils.rs b/src/user/application/utils.rs new file mode 100644 index 0000000..de68c2e --- /dev/null +++ b/src/user/application/utils.rs @@ -0,0 +1,36 @@ +use validator::ValidateEmail; + +use super::port::out::get_user; +use crate::user::domain::{User, UserResult}; +use get_user::*; + +pub(super) fn fetch_user_from_login(get_user: &dyn GetUserPort, login: &str) -> UserResult { + if login.validate_email() { + get_user.get_user_by_email(login) + } else { + get_user.get_user_by_name(login) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::user::domain::tests::utils::*; + + use crate::user::application::port::out::tests::utils::MockOutPortAdapter; + + #[test] + fn test_fetch_user_from_login() { + let u = get_user(); + let adapter: Box = Box::new(MockOutPortAdapter::new()); + + assert_eq!( + fetch_user_from_login(adapter.as_ref(), u.name()).unwrap(), + u + ); + assert_eq!( + fetch_user_from_login(adapter.as_ref(), u.email()).unwrap(), + u + ); + } +} diff --git a/src/user/domain/mod.rs b/src/user/domain/mod.rs new file mode 100644 index 0000000..c416658 --- /dev/null +++ b/src/user/domain/mod.rs @@ -0,0 +1,93 @@ +use derive_more::Display; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct User { + id: usize, + name: String, + password: String, + email: String, +} + +impl User { + // getters + pub fn id(&self) -> usize { + self.id + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn password(&self) -> &str { + &self.password + } + pub fn email(&self) -> &str { + &self.email + } + + pub(crate) fn set_password(&mut self, password: String) { + self.password = password; + } + + pub(crate) fn new(id: usize, name: String, password: String, email: String) -> Self { + Self { + id, + name, + password, + email, + } + } +} + +pub type UserResult = Result; + +#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum UserError { + DuplicateUsername, + DuplicateEmail, + InternalError, + PasswordsDontMatch, + PasswordAlreadyUsed, + PasswordIsEmpty, + WrongPassword, + NotAnEmail, + UsernameIsEmpty, + LoginIsEmpty, +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + + pub(crate) mod utils { + use super::*; + + pub fn get_user() -> User { + let password = "password"; + let email = "user1@foo.com"; + let name = "user1"; + + User::new(1, name.into(), password.into(), email.into()) + } + + pub fn get_user_by_id(id: usize) -> User { + let mut u = get_user(); + + u.id = id; + u + } + + pub fn get_user_by_name(name: String) -> User { + let mut u = get_user(); + u.name = name; + u + } + + pub fn get_user_by_email(email: String) -> User { + let mut u = get_user(); + u.email = email; + u + } + } +} diff --git a/src/user/mod.rs b/src/user/mod.rs new file mode 100644 index 0000000..430f620 --- /dev/null +++ b/src/user/mod.rs @@ -0,0 +1,3 @@ +pub mod adapter; +pub mod application; +pub mod domain;