This commit is contained in:
Aravinth Manivannan 2024-04-23 14:10:05 +05:30
commit bbce466a0e
Signed by: realaravinth
GPG Key ID: F8F50389936984FF
29 changed files with 1238 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

266
Cargo.lock generated Normal file
View File

@ -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",
]

11
Cargo.toml Normal file
View File

@ -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"

7
src/main.rs Normal file
View File

@ -0,0 +1,7 @@
//#![allow(dead_code)]
//mod project;
mod user;
fn main() {
println!("Hello, world!");
}

116
src/project.rs Normal file
View File

@ -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<String>,
members: Vec<User>,
}
pub mod services {
use super::*;
pub fn get_projects_belonging_to_user(
project_adapter: &dyn SecondaryPortProject,
owner: &User,
) -> ProjectResult<Vec<Project>> {
project_adapter.get_projects_belonging_to_user(owner.id())
}
pub fn create(
project_adapter: &dyn SecondaryPortProject,
owner: User,
name: String,
description: Option<String>,
) -> ProjectResult<Project> {
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<bool> {
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<Project> {
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<String>, members: Vec<User>) -> 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<String>, members: Vec<User>) -> Self {
Self::new(id, name, description, members)
}
}
pub type ProjectResult<V> = Result<V, ProjectError>;
#[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<usize>;
fn update_description(&self, id: usize, new_description: &str) -> ProjectResult<Project>;
// get user from DB
fn get_project_by_id(&self, id: usize) -> ProjectResult<Project>;
fn get_projects_belonging_to_user(&self, user_id: usize) -> ProjectResult<Vec<Project>>;
}

View File

1
src/user/adapter/mod.rs Normal file
View File

@ -0,0 +1 @@

View File

View File

@ -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<Self, UserError> {
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())
}
}

View File

@ -0,0 +1,2 @@
pub mod command;
pub mod service;

View File

@ -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<User>;
}
pub struct FetchUserService {
get_user: Box<dyn GetUserPort>,
}
impl FetchUserService {
pub fn new(get_user: Box<dyn GetUserPort>) -> Self {
Self { get_user }
}
}
impl FetchUserTrait for FetchUserService {
fn fetch_user_from_login(&self, cmd: command::FetchUserCommand) -> UserResult<User> {
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
);
}
}

View File

@ -0,0 +1,6 @@
mod fetch_user_service;
mod port;
mod register_service;
mod signin_service;
mod update_user_password_service;
mod utils;

View File

@ -0,0 +1 @@
pub mod out;

View File

@ -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<()>;
}

View File

@ -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<User>;
// get user from DB
fn get_user_by_email(&self, email: &str) -> UserResult<User>;
}

View File

@ -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<RwLock<Vec<User>>>,
}
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<User> {
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<User> {
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(())
}
}
}
}

View File

@ -0,0 +1,5 @@
use crate::user::domain::UserResult;
pub trait UpdatePasswordPort {
fn update_password(&self, id: usize, new_password: &str) -> UserResult<()>;
}

View File

@ -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<Self, UserError> {
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()
}
}
}
}

View File

@ -0,0 +1,2 @@
pub mod command;
pub mod service;

View File

@ -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<dyn create_user::CreateUserPort>,
}
impl RegisterUserService {
pub fn new(crate_user_adapter: Box<dyn create_user::CreateUserPort>) -> 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"
)
}
}

View File

@ -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<Self, UserError> {
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(),);
}
}

View File

@ -0,0 +1,2 @@
pub mod command;
pub mod service;

View File

@ -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<dyn GetUserPort>,
}
impl SigninUserService {
pub fn new(get_user: Box<dyn GetUserPort>) -> 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());
}
}

View File

@ -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<Self, UserError> {
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)
);
}
}

View File

@ -0,0 +1,2 @@
pub mod command;
pub mod service;

View File

@ -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<dyn GetUserPort>,
update_password: Box<dyn UpdatePasswordPort>,
}
impl UpdateUserPasswordService {
pub fn new(
get_user: Box<dyn GetUserPort>,
update_password: Box<dyn UpdatePasswordPort>,
) -> 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);
}
}

View File

@ -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<User> {
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<dyn GetUserPort> = 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
);
}
}

93
src/user/domain/mod.rs Normal file
View File

@ -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<V> = Result<V, UserError>;
#[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
}
}
}

3
src/user/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod adapter;
pub mod application;
pub mod domain;