Add REST endpoint for slot
This commit is contained in:
parent
82e89baeeb
commit
8f378472ea
28 changed files with 1925 additions and 28 deletions
64
Cargo.lock
generated
64
Cargo.lock
generated
|
|
@ -51,6 +51,8 @@ dependencies = [
|
||||||
"rest",
|
"rest",
|
||||||
"service_impl",
|
"service_impl",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"time",
|
||||||
|
"time-macros",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -272,6 +274,7 @@ dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"mockall",
|
"mockall",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"time",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -282,6 +285,8 @@ dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"dao",
|
"dao",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"time",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -295,6 +300,16 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.3.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
|
|
@ -849,6 +864,12 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.46"
|
version = "0.1.46"
|
||||||
|
|
@ -1007,6 +1028,12 @@ version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
|
|
@ -1115,6 +1142,8 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"service",
|
"service",
|
||||||
|
"thiserror",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
@ -1237,7 +1266,9 @@ dependencies = [
|
||||||
"dao",
|
"dao",
|
||||||
"mockall",
|
"mockall",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1248,7 +1279,9 @@ dependencies = [
|
||||||
"dao",
|
"dao",
|
||||||
"mockall",
|
"mockall",
|
||||||
"service",
|
"service",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1635,6 +1668,37 @@ dependencies = [
|
||||||
"syn 2.0.60",
|
"syn 2.0.60",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"itoa",
|
||||||
|
"num-conv",
|
||||||
|
"powerfmt",
|
||||||
|
"serde",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["rest", "service", "service_impl", "app", "dao", "dao_impl"]
|
members = ["rest", "service", "service_impl", "app", "dao", "dao_impl"]
|
||||||
|
resolver = "2"
|
||||||
|
|
|
||||||
|
|
@ -24,3 +24,9 @@ features = ["full"]
|
||||||
[dependencies.sqlx]
|
[dependencies.sqlx]
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
features = ["runtime-tokio", "sqlite"]
|
features = ["runtime-tokio", "sqlite"]
|
||||||
|
|
||||||
|
[dependencies.time]
|
||||||
|
version = "0.3.36"
|
||||||
|
|
||||||
|
[dependencies.time-macros]
|
||||||
|
version = "0.2.18"
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,25 @@ use sqlx::SqlitePool;
|
||||||
type PermissionService =
|
type PermissionService =
|
||||||
service_impl::PermissionServiceImpl<dao_impl::PermissionDaoImpl, service_impl::UserServiceDev>;
|
service_impl::PermissionServiceImpl<dao_impl::PermissionDaoImpl, service_impl::UserServiceDev>;
|
||||||
type HelloService = service_impl::HelloServiceImpl<dao_impl::HelloDaoImpl, PermissionService>;
|
type HelloService = service_impl::HelloServiceImpl<dao_impl::HelloDaoImpl, PermissionService>;
|
||||||
|
type ClockService = service_impl::clock::ClockServiceImpl;
|
||||||
|
type UuidService = service_impl::uuid_service::UuidServiceImpl;
|
||||||
|
type SlotService = service_impl::slot::SlotServiceImpl<
|
||||||
|
dao_impl::slot::SlotDaoImpl,
|
||||||
|
PermissionService,
|
||||||
|
ClockService,
|
||||||
|
UuidService,
|
||||||
|
>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RestStateImpl {
|
pub struct RestStateImpl {
|
||||||
hello_service: Arc<HelloService>,
|
hello_service: Arc<HelloService>,
|
||||||
permission_service: Arc<PermissionService>,
|
permission_service: Arc<PermissionService>,
|
||||||
|
slot_service: Arc<SlotService>,
|
||||||
}
|
}
|
||||||
impl rest::RestStateDef for RestStateImpl {
|
impl rest::RestStateDef for RestStateImpl {
|
||||||
type HelloService = HelloService;
|
type HelloService = HelloService;
|
||||||
type PermissionService = PermissionService;
|
type PermissionService = PermissionService;
|
||||||
|
type SlotService = SlotService;
|
||||||
|
|
||||||
fn hello_service(&self) -> Arc<Self::HelloService> {
|
fn hello_service(&self) -> Arc<Self::HelloService> {
|
||||||
self.hello_service.clone()
|
self.hello_service.clone()
|
||||||
|
|
@ -21,11 +31,15 @@ impl rest::RestStateDef for RestStateImpl {
|
||||||
fn permission_service(&self) -> Arc<Self::PermissionService> {
|
fn permission_service(&self) -> Arc<Self::PermissionService> {
|
||||||
self.permission_service.clone()
|
self.permission_service.clone()
|
||||||
}
|
}
|
||||||
|
fn slot_service(&self) -> Arc<Self::SlotService> {
|
||||||
|
self.slot_service.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl RestStateImpl {
|
impl RestStateImpl {
|
||||||
pub fn new(pool: Arc<sqlx::Pool<sqlx::Sqlite>>) -> Self {
|
pub fn new(pool: Arc<sqlx::Pool<sqlx::Sqlite>>) -> Self {
|
||||||
let hello_dao = dao_impl::HelloDaoImpl::new(pool.clone());
|
let hello_dao = dao_impl::HelloDaoImpl::new(pool.clone());
|
||||||
let permission_dao = dao_impl::PermissionDaoImpl::new(pool);
|
let permission_dao = dao_impl::PermissionDaoImpl::new(pool.clone());
|
||||||
|
let slot_dao = dao_impl::slot::SlotDaoImpl::new(pool);
|
||||||
|
|
||||||
// Always authenticate with DEVUSER during development.
|
// Always authenticate with DEVUSER during development.
|
||||||
// This is used to test the permission service locally without a login service.
|
// This is used to test the permission service locally without a login service.
|
||||||
|
|
@ -42,9 +56,18 @@ impl RestStateImpl {
|
||||||
hello_dao.into(),
|
hello_dao.into(),
|
||||||
permission_service.clone(),
|
permission_service.clone(),
|
||||||
));
|
));
|
||||||
|
let clock_service = Arc::new(service_impl::clock::ClockServiceImpl);
|
||||||
|
let uuid_service = Arc::new(service_impl::uuid_service::UuidServiceImpl);
|
||||||
|
let slot_service = Arc::new(service_impl::slot::SlotServiceImpl::new(
|
||||||
|
slot_dao.into(),
|
||||||
|
permission_service.clone(),
|
||||||
|
clock_service,
|
||||||
|
uuid_service,
|
||||||
|
));
|
||||||
Self {
|
Self {
|
||||||
hello_service,
|
hello_service,
|
||||||
permission_service,
|
permission_service,
|
||||||
|
slot_service,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,7 @@ async-trait = "0.1.80"
|
||||||
mockall = "0.12.1"
|
mockall = "0.12.1"
|
||||||
thiserror = "1.0.59"
|
thiserror = "1.0.59"
|
||||||
uuid = "1.8"
|
uuid = "1.8"
|
||||||
|
|
||||||
|
[dependencies.time]
|
||||||
|
version = "0.3.36"
|
||||||
|
features = ["parsing"]
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ use async_trait::async_trait;
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
mod permission;
|
pub mod permission;
|
||||||
|
pub mod slot;
|
||||||
|
|
||||||
pub use permission::MockPermissionDao;
|
pub use permission::MockPermissionDao;
|
||||||
pub use permission::PermissionDao;
|
pub use permission::PermissionDao;
|
||||||
|
|
@ -15,7 +16,16 @@ pub use permission::UserEntity;
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum DaoError {
|
pub enum DaoError {
|
||||||
#[error("Database query error: {0}")]
|
#[error("Database query error: {0}")]
|
||||||
DatabaseQueryError(#[from] Box<dyn std::error::Error>),
|
DatabaseQueryError(#[from] Box<dyn std::error::Error + Send + Sync>),
|
||||||
|
|
||||||
|
#[error("Uuid error: {0}")]
|
||||||
|
UuidError(#[from] uuid::Error),
|
||||||
|
|
||||||
|
#[error("Invalid day of week number: {0}")]
|
||||||
|
InvalidDayOfWeek(u8),
|
||||||
|
|
||||||
|
#[error("Date/Time parse error: {0}")]
|
||||||
|
DateTimeParseError(#[from] time::error::Parse),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[automock]
|
#[automock]
|
||||||
|
|
|
||||||
65
dao/src/slot.rs
Normal file
65
dao/src/slot.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use mockall::automock;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::DaoError;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum DayOfWeek {
|
||||||
|
Monday,
|
||||||
|
Tuesday,
|
||||||
|
Wednesday,
|
||||||
|
Thursday,
|
||||||
|
Friday,
|
||||||
|
Saturday,
|
||||||
|
Sunday,
|
||||||
|
}
|
||||||
|
impl DayOfWeek {
|
||||||
|
pub fn from_number(number: u8) -> Option<Self> {
|
||||||
|
match number {
|
||||||
|
1 => Some(DayOfWeek::Monday),
|
||||||
|
2 => Some(DayOfWeek::Tuesday),
|
||||||
|
3 => Some(DayOfWeek::Wednesday),
|
||||||
|
4 => Some(DayOfWeek::Thursday),
|
||||||
|
5 => Some(DayOfWeek::Friday),
|
||||||
|
6 => Some(DayOfWeek::Saturday),
|
||||||
|
7 => Some(DayOfWeek::Sunday),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn to_number(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
DayOfWeek::Monday => 1,
|
||||||
|
DayOfWeek::Tuesday => 2,
|
||||||
|
DayOfWeek::Wednesday => 3,
|
||||||
|
DayOfWeek::Thursday => 4,
|
||||||
|
DayOfWeek::Friday => 5,
|
||||||
|
DayOfWeek::Saturday => 6,
|
||||||
|
DayOfWeek::Sunday => 7,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct SlotEntity {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub day_of_week: DayOfWeek,
|
||||||
|
pub from: time::Time,
|
||||||
|
pub to: time::Time,
|
||||||
|
pub valid_from: time::Date,
|
||||||
|
pub valid_to: Option<time::Date>,
|
||||||
|
pub deleted: Option<time::PrimitiveDateTime>,
|
||||||
|
pub version: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[automock]
|
||||||
|
#[async_trait]
|
||||||
|
pub trait SlotDao {
|
||||||
|
async fn get_slots(&self) -> Result<Arc<[SlotEntity]>, DaoError>;
|
||||||
|
async fn get_slot(&self, id: &Uuid) -> Result<Option<SlotEntity>, DaoError>;
|
||||||
|
async fn create_slot(&self, slot: &SlotEntity, process: &str) -> Result<(), DaoError>;
|
||||||
|
//async fn delete_slot(&self, id: &Uuid, process: &str) -> Result<(), DaoError>;
|
||||||
|
async fn update_slot(&self, slot: &SlotEntity, process: &str) -> Result<(), DaoError>;
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1.80"
|
async-trait = "0.1.80"
|
||||||
|
uuid = "1.8.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
|
@ -14,3 +15,7 @@ features = ["runtime-tokio", "sqlite"]
|
||||||
|
|
||||||
[dependencies.dao]
|
[dependencies.dao]
|
||||||
path = "../dao"
|
path = "../dao"
|
||||||
|
|
||||||
|
[dependencies.time]
|
||||||
|
version = "0.3.36"
|
||||||
|
features = ["parsing"]
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@ use async_trait::async_trait;
|
||||||
use dao::DaoError;
|
use dao::DaoError;
|
||||||
use sqlx::{query, query_as, SqlitePool};
|
use sqlx::{query, query_as, SqlitePool};
|
||||||
|
|
||||||
|
pub mod slot;
|
||||||
|
|
||||||
pub trait ResultDbErrorExt<T, E> {
|
pub trait ResultDbErrorExt<T, E> {
|
||||||
fn map_db_error(self) -> Result<T, DaoError>;
|
fn map_db_error(self) -> Result<T, DaoError>;
|
||||||
}
|
}
|
||||||
impl<T, E: std::error::Error + 'static> ResultDbErrorExt<T, E> for Result<T, E> {
|
impl<T, E: std::error::Error + Send + Sync + 'static> ResultDbErrorExt<T, E> for Result<T, E> {
|
||||||
fn map_db_error(self) -> Result<T, DaoError> {
|
fn map_db_error(self) -> Result<T, DaoError> {
|
||||||
self.map_err(|err| DaoError::DatabaseQueryError(Box::new(err)))
|
self.map_err(|err| DaoError::DatabaseQueryError(Box::new(err)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
128
dao_impl/src/slot.rs
Normal file
128
dao_impl/src/slot.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use dao::{
|
||||||
|
slot::{DayOfWeek, SlotEntity},
|
||||||
|
DaoError,
|
||||||
|
};
|
||||||
|
use sqlx::{query, SqlitePool};
|
||||||
|
use time::{format_description::well_known::Iso8601, Date, PrimitiveDateTime, Time};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::ResultDbErrorExt;
|
||||||
|
|
||||||
|
pub struct SlotDaoImpl {
|
||||||
|
pool: Arc<SqlitePool>,
|
||||||
|
}
|
||||||
|
impl SlotDaoImpl {
|
||||||
|
pub fn new(pool: Arc<SqlitePool>) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl dao::slot::SlotDao for SlotDaoImpl {
|
||||||
|
async fn get_slots(&self) -> Result<Arc<[SlotEntity]>, DaoError> {
|
||||||
|
let result = query!(r"SELECT id, day_of_week, time_from, time_to, valid_from, valid_to, deleted, update_version FROM slot WHERE deleted IS NULL")
|
||||||
|
.fetch_all(self.pool.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|err| DaoError::DatabaseQueryError(Box::new(err)))?;
|
||||||
|
result
|
||||||
|
.iter()
|
||||||
|
.map(|row| {
|
||||||
|
Ok(SlotEntity {
|
||||||
|
id: Uuid::from_slice(row.id.as_ref())?,
|
||||||
|
day_of_week: DayOfWeek::from_number(row.day_of_week as u8)
|
||||||
|
.ok_or(DaoError::InvalidDayOfWeek(row.day_of_week as u8))?,
|
||||||
|
from: Time::parse(&row.time_from, &Iso8601::TIME)?,
|
||||||
|
to: Time::parse(&row.time_to, &Iso8601::TIME)?,
|
||||||
|
valid_from: Date::parse(&row.valid_from, &Iso8601::DATE)?,
|
||||||
|
valid_to: row
|
||||||
|
.valid_to
|
||||||
|
.as_ref()
|
||||||
|
.map(|valid_to| Date::parse(valid_to, &Iso8601::DATE))
|
||||||
|
.transpose()?,
|
||||||
|
deleted: row
|
||||||
|
.deleted
|
||||||
|
.as_ref()
|
||||||
|
.map(|deleted| PrimitiveDateTime::parse(deleted, &Iso8601::DATE))
|
||||||
|
.transpose()?,
|
||||||
|
version: Uuid::from_slice(&row.update_version)?,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
async fn get_slot(&self, id: &Uuid) -> Result<Option<SlotEntity>, DaoError> {
|
||||||
|
let id_vec = id.as_bytes().to_vec();
|
||||||
|
let result = query!(r"SELECT id, day_of_week, time_from, time_to, valid_from, valid_to, deleted, update_version FROM slot WHERE id = ?", id_vec)
|
||||||
|
.fetch_optional(self.pool.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(|err| DaoError::DatabaseQueryError(Box::new(err)))?;
|
||||||
|
result
|
||||||
|
.map(|row| {
|
||||||
|
Ok(SlotEntity {
|
||||||
|
id: Uuid::from_slice(row.id.as_ref())?,
|
||||||
|
day_of_week: DayOfWeek::from_number(row.day_of_week as u8)
|
||||||
|
.ok_or(DaoError::InvalidDayOfWeek(row.day_of_week as u8))?,
|
||||||
|
from: Time::parse(&row.time_from, &Iso8601::TIME)?,
|
||||||
|
to: Time::parse(&row.time_to, &Iso8601::TIME)?,
|
||||||
|
valid_from: Date::parse(&row.valid_from, &Iso8601::DATE)?,
|
||||||
|
valid_to: row
|
||||||
|
.valid_to
|
||||||
|
.as_ref()
|
||||||
|
.map(|valid_to| Date::parse(valid_to, &Iso8601::DATE))
|
||||||
|
.transpose()?,
|
||||||
|
deleted: row
|
||||||
|
.deleted
|
||||||
|
.as_ref()
|
||||||
|
.map(|deleted| PrimitiveDateTime::parse(deleted, &Iso8601::DATE))
|
||||||
|
.transpose()?,
|
||||||
|
version: Uuid::from_slice(&row.update_version)?,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()
|
||||||
|
}
|
||||||
|
async fn create_slot(&self, slot: &SlotEntity, process: &str) -> Result<(), DaoError> {
|
||||||
|
let id_vec = slot.id.as_bytes().to_vec();
|
||||||
|
let version_vec = slot.version.as_bytes().to_vec();
|
||||||
|
let day_of_week = slot.day_of_week.to_number();
|
||||||
|
let from = slot.from.to_string();
|
||||||
|
let to = slot.to.to_string();
|
||||||
|
let valid_from = slot.valid_from.to_string();
|
||||||
|
let valid_to = slot.valid_to.map(|valid_to| valid_to.to_string());
|
||||||
|
let deleted = slot.deleted.map(|deleted| deleted.to_string());
|
||||||
|
query!("INSERT INTO slot (id, day_of_week, time_from, time_to, valid_from, valid_to, deleted, update_version, update_process) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
id_vec,
|
||||||
|
day_of_week,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
valid_from,
|
||||||
|
valid_to,
|
||||||
|
deleted,
|
||||||
|
version_vec,
|
||||||
|
process,
|
||||||
|
)
|
||||||
|
.execute(self.pool.as_ref())
|
||||||
|
.await
|
||||||
|
.map_db_error()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_slot(&self, slot: &SlotEntity, process: &str) -> Result<(), DaoError> {
|
||||||
|
let id_vec = slot.id.as_bytes().to_vec();
|
||||||
|
let version_vec = slot.version.as_bytes().to_vec();
|
||||||
|
let valid_to = slot.valid_to.map(|valid_to| valid_to.to_string());
|
||||||
|
let deleted = slot.deleted.map(|deleted| deleted.to_string());
|
||||||
|
query!("UPDATE slot SET valid_to = ?, deleted = ?, update_version = ?, update_process = ? WHERE id = ?",
|
||||||
|
valid_to,
|
||||||
|
deleted,
|
||||||
|
version_vec,
|
||||||
|
process,
|
||||||
|
id_vec,
|
||||||
|
)
|
||||||
|
.execute(self.pool.as_ref())
|
||||||
|
.await
|
||||||
|
.map_db_error()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
15
migrations/20240502113031_add-slot.sql
Normal file
15
migrations/20240502113031_add-slot.sql
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
-- Add migration script here
|
||||||
|
|
||||||
|
CREATE TABLE slot (
|
||||||
|
id blob(16) NOT NULL PRIMARY KEY,
|
||||||
|
day_of_week INTEGER NOT NULL,
|
||||||
|
time_from TEXT NOT NULL,
|
||||||
|
time_to TEXT NOT NULL,
|
||||||
|
valid_from TEXT NOT NULL,
|
||||||
|
valid_to TEXT,
|
||||||
|
deleted TEXT,
|
||||||
|
|
||||||
|
update_timestamp TEXT,
|
||||||
|
update_process TEXT NOT NULL,
|
||||||
|
update_version blob(16) NOT NULL
|
||||||
|
);
|
||||||
|
|
@ -9,8 +9,8 @@ edition = "2021"
|
||||||
axum = "0.7.5"
|
axum = "0.7.5"
|
||||||
bytes = "1.6.0"
|
bytes = "1.6.0"
|
||||||
http-body = "1.0.0"
|
http-body = "1.0.0"
|
||||||
serde = "1.0.198"
|
|
||||||
serde_json = "1.0.116"
|
serde_json = "1.0.116"
|
||||||
|
time = { version = "0.3.36", features = ["serde-human-readable"] }
|
||||||
|
|
||||||
[dependencies.tokio]
|
[dependencies.tokio]
|
||||||
version = "1.37.0"
|
version = "1.37.0"
|
||||||
|
|
@ -22,3 +22,10 @@ path = "../service"
|
||||||
[dependencies.uuid]
|
[dependencies.uuid]
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
features = ["v4", "serde"]
|
features = ["v4", "serde"]
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
version = "1.0.198"
|
||||||
|
features = ["derive", "std", "alloc", "rc"]
|
||||||
|
|
||||||
|
[dependencies.thiserror]
|
||||||
|
version = "1.0"
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,11 @@ use std::{convert::Infallible, sync::Arc};
|
||||||
|
|
||||||
mod hello;
|
mod hello;
|
||||||
mod permission;
|
mod permission;
|
||||||
|
mod slot;
|
||||||
|
|
||||||
use axum::{body::Body, response::Response, routing::get, Router};
|
use axum::{body::Body, response::Response, routing::get, Router};
|
||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub struct RoString(Arc<str>, bool);
|
pub struct RoString(Arc<str>, bool);
|
||||||
impl http_body::Body for RoString {
|
impl http_body::Body for RoString {
|
||||||
|
|
@ -39,31 +42,103 @@ impl From<RoString> for Response {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn error_handler(result: Result<Response, service::ServiceError>) -> Response {
|
#[derive(Debug, Error)]
|
||||||
|
pub enum RestError {
|
||||||
|
#[error("Service error")]
|
||||||
|
ServiceError(#[from] service::ServiceError),
|
||||||
|
|
||||||
|
#[error("Inconsistent id. Got {0} in path but {1} in body")]
|
||||||
|
InconsistentId(Uuid, Uuid),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_handler(result: Result<Response, RestError>) -> Response {
|
||||||
match result {
|
match result {
|
||||||
Ok(response) => response,
|
Ok(response) => response,
|
||||||
Err(service::ServiceError::Forbidden) => {
|
Err(err @ RestError::InconsistentId(_, _)) => Response::builder()
|
||||||
|
.status(400)
|
||||||
|
.body(Body::new(err.to_string()))
|
||||||
|
.unwrap(),
|
||||||
|
Err(RestError::ServiceError(service::ServiceError::Forbidden)) => {
|
||||||
Response::builder().status(403).body(Body::empty()).unwrap()
|
Response::builder().status(403).body(Body::empty()).unwrap()
|
||||||
}
|
}
|
||||||
Err(service::ServiceError::DatabaseQueryError(e)) => Response::builder()
|
Err(RestError::ServiceError(service::ServiceError::DatabaseQueryError(e))) => {
|
||||||
|
Response::builder()
|
||||||
.status(500)
|
.status(500)
|
||||||
.body(Body::new(e.to_string()))
|
.body(Body::new(e.to_string()))
|
||||||
.unwrap(),
|
.unwrap()
|
||||||
|
}
|
||||||
|
Err(RestError::ServiceError(service::ServiceError::EntityAlreadyExists(id))) => {
|
||||||
|
Response::builder()
|
||||||
|
.status(409)
|
||||||
|
.body(Body::new(id.to_string()))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
Err(RestError::ServiceError(service::ServiceError::EntityNotFound(id))) => {
|
||||||
|
Response::builder()
|
||||||
|
.status(404)
|
||||||
|
.body(Body::new(id.to_string()))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
Err(RestError::ServiceError(err @ service::ServiceError::EntityConflicts(_, _, _))) => {
|
||||||
|
Response::builder()
|
||||||
|
.status(409)
|
||||||
|
.body(Body::new(err.to_string()))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
Err(RestError::ServiceError(err @ service::ServiceError::ValidationError(_))) => {
|
||||||
|
Response::builder()
|
||||||
|
.status(422)
|
||||||
|
.body(Body::new(err.to_string()))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
Err(RestError::ServiceError(err @ service::ServiceError::IdSetOnCreate)) => {
|
||||||
|
Response::builder()
|
||||||
|
.status(422)
|
||||||
|
.body(Body::new(err.to_string()))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
Err(RestError::ServiceError(err @ service::ServiceError::VersionSetOnCreate)) => {
|
||||||
|
Response::builder()
|
||||||
|
.status(422)
|
||||||
|
.body(Body::new(err.to_string()))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
Err(RestError::ServiceError(err @ service::ServiceError::OverlappingTimeRange)) => {
|
||||||
|
Response::builder()
|
||||||
|
.status(409)
|
||||||
|
.body(Body::new(err.to_string()))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
Err(RestError::ServiceError(err @ service::ServiceError::TimeOrderWrong(_, _))) => {
|
||||||
|
Response::builder()
|
||||||
|
.status(422)
|
||||||
|
.body(Body::new(err.to_string()))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
Err(RestError::ServiceError(err @ service::ServiceError::DateOrderWrong(_, _))) => {
|
||||||
|
Response::builder()
|
||||||
|
.status(422)
|
||||||
|
.body(Body::new(err.to_string()))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait RestStateDef: Clone + Send + Sync + 'static {
|
pub trait RestStateDef: Clone + Send + Sync + 'static {
|
||||||
type HelloService: service::HelloService + Send + Sync + 'static;
|
type HelloService: service::HelloService + Send + Sync + 'static;
|
||||||
type PermissionService: service::PermissionService + Send + Sync + 'static;
|
type PermissionService: service::PermissionService + Send + Sync + 'static;
|
||||||
|
type SlotService: service::slot::SlotService + Send + Sync + 'static;
|
||||||
|
|
||||||
fn hello_service(&self) -> Arc<Self::HelloService>;
|
fn hello_service(&self) -> Arc<Self::HelloService>;
|
||||||
fn permission_service(&self) -> Arc<Self::PermissionService>;
|
fn permission_service(&self) -> Arc<Self::PermissionService>;
|
||||||
|
fn slot_service(&self) -> Arc<Self::SlotService>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_server<RestState: RestStateDef>(rest_state: RestState) {
|
pub async fn start_server<RestState: RestStateDef>(rest_state: RestState) {
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(hello::hello::<RestState>))
|
.route("/", get(hello::hello::<RestState>))
|
||||||
.nest("/permission", permission::generate_route())
|
.nest("/permission", permission::generate_route())
|
||||||
|
.nest("/slot", slot::generate_route())
|
||||||
.with_state(rest_state);
|
.with_state(rest_state);
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,10 @@ use crate::{error_handler, RestStateDef};
|
||||||
use service::PermissionService;
|
use service::PermissionService;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct User {
|
pub struct UserTO {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
impl From<&service::User> for User {
|
impl From<&service::User> for UserTO {
|
||||||
fn from(user: &service::User) -> Self {
|
fn from(user: &service::User) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: user.name.to_string(),
|
name: user.name.to_string(),
|
||||||
|
|
@ -25,10 +25,10 @@ impl From<&service::User> for User {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Role {
|
pub struct RoleTO {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
impl From<&service::Role> for Role {
|
impl From<&service::Role> for RoleTO {
|
||||||
fn from(role: &service::Role) -> Self {
|
fn from(role: &service::Role) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: role.name.to_string(),
|
name: role.name.to_string(),
|
||||||
|
|
@ -37,10 +37,10 @@ impl From<&service::Role> for Role {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Privilege {
|
pub struct PrivilegeTO {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
impl From<&service::Privilege> for Privilege {
|
impl From<&service::Privilege> for PrivilegeTO {
|
||||||
fn from(privilege: &service::Privilege) -> Self {
|
fn from(privilege: &service::Privilege) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: privilege.name.to_string(),
|
name: privilege.name.to_string(),
|
||||||
|
|
@ -80,7 +80,7 @@ pub fn generate_route<RestState: RestStateDef>() -> Router<RestState> {
|
||||||
|
|
||||||
pub async fn add_user<RestState: RestStateDef>(
|
pub async fn add_user<RestState: RestStateDef>(
|
||||||
rest_state: State<RestState>,
|
rest_state: State<RestState>,
|
||||||
Json(user): Json<User>,
|
Json(user): Json<UserTO>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
println!("Adding user: {:?}", user);
|
println!("Adding user: {:?}", user);
|
||||||
error_handler(
|
error_handler(
|
||||||
|
|
@ -117,7 +117,7 @@ pub async fn remove_user<RestState: RestStateDef>(
|
||||||
|
|
||||||
pub async fn add_role<RestState: RestStateDef>(
|
pub async fn add_role<RestState: RestStateDef>(
|
||||||
rest_state: State<RestState>,
|
rest_state: State<RestState>,
|
||||||
Json(role): Json<Role>,
|
Json(role): Json<RoleTO>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
error_handler(
|
error_handler(
|
||||||
(async {
|
(async {
|
||||||
|
|
@ -238,12 +238,12 @@ pub async fn remove_role_privilege<RestState: RestStateDef>(
|
||||||
pub async fn get_all_users<RestState: RestStateDef>(rest_state: State<RestState>) -> Response {
|
pub async fn get_all_users<RestState: RestStateDef>(rest_state: State<RestState>) -> Response {
|
||||||
error_handler(
|
error_handler(
|
||||||
(async {
|
(async {
|
||||||
let users: Arc<[User]> = rest_state
|
let users: Arc<[UserTO]> = rest_state
|
||||||
.permission_service()
|
.permission_service()
|
||||||
.get_all_users()
|
.get_all_users()
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.map(User::from)
|
.map(UserTO::from)
|
||||||
.collect();
|
.collect();
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(200)
|
.status(200)
|
||||||
|
|
@ -257,12 +257,12 @@ pub async fn get_all_users<RestState: RestStateDef>(rest_state: State<RestState>
|
||||||
pub async fn get_all_roles<RestState: RestStateDef>(rest_state: State<RestState>) -> Response {
|
pub async fn get_all_roles<RestState: RestStateDef>(rest_state: State<RestState>) -> Response {
|
||||||
error_handler(
|
error_handler(
|
||||||
(async {
|
(async {
|
||||||
let roles: Arc<[Role]> = rest_state
|
let roles: Arc<[RoleTO]> = rest_state
|
||||||
.permission_service()
|
.permission_service()
|
||||||
.get_all_roles()
|
.get_all_roles()
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.map(Role::from)
|
.map(RoleTO::from)
|
||||||
.collect();
|
.collect();
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(200)
|
.status(200)
|
||||||
|
|
@ -276,12 +276,12 @@ pub async fn get_all_roles<RestState: RestStateDef>(rest_state: State<RestState>
|
||||||
pub async fn get_all_privileges<RestState: RestStateDef>(rest_state: State<RestState>) -> Response {
|
pub async fn get_all_privileges<RestState: RestStateDef>(rest_state: State<RestState>) -> Response {
|
||||||
error_handler(
|
error_handler(
|
||||||
(async {
|
(async {
|
||||||
let privileges: Arc<[Privilege]> = rest_state
|
let privileges: Arc<[PrivilegeTO]> = rest_state
|
||||||
.permission_service()
|
.permission_service()
|
||||||
.get_all_privileges()
|
.get_all_privileges()
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.map(Privilege::from)
|
.map(PrivilegeTO::from)
|
||||||
.collect();
|
.collect();
|
||||||
Ok(Response::builder()
|
Ok(Response::builder()
|
||||||
.status(200)
|
.status(200)
|
||||||
|
|
|
||||||
182
rest/src/slot.rs
Normal file
182
rest/src/slot.rs
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
extract::{Path, State},
|
||||||
|
response::Response,
|
||||||
|
routing::{get, post, put},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use service::slot::SlotService;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{error_handler, RestError, RestStateDef};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub enum DayOfWeek {
|
||||||
|
Monday,
|
||||||
|
Tuesday,
|
||||||
|
Wednesday,
|
||||||
|
Thursday,
|
||||||
|
Friday,
|
||||||
|
Saturday,
|
||||||
|
Sunday,
|
||||||
|
}
|
||||||
|
impl From<service::slot::DayOfWeek> for DayOfWeek {
|
||||||
|
fn from(day_of_week: service::slot::DayOfWeek) -> Self {
|
||||||
|
match day_of_week {
|
||||||
|
service::slot::DayOfWeek::Monday => Self::Monday,
|
||||||
|
service::slot::DayOfWeek::Tuesday => Self::Tuesday,
|
||||||
|
service::slot::DayOfWeek::Wednesday => Self::Wednesday,
|
||||||
|
service::slot::DayOfWeek::Thursday => Self::Thursday,
|
||||||
|
service::slot::DayOfWeek::Friday => Self::Friday,
|
||||||
|
service::slot::DayOfWeek::Saturday => Self::Saturday,
|
||||||
|
service::slot::DayOfWeek::Sunday => Self::Sunday,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<DayOfWeek> for service::slot::DayOfWeek {
|
||||||
|
fn from(day_of_week: DayOfWeek) -> Self {
|
||||||
|
match day_of_week {
|
||||||
|
DayOfWeek::Monday => Self::Monday,
|
||||||
|
DayOfWeek::Tuesday => Self::Tuesday,
|
||||||
|
DayOfWeek::Wednesday => Self::Wednesday,
|
||||||
|
DayOfWeek::Thursday => Self::Thursday,
|
||||||
|
DayOfWeek::Friday => Self::Friday,
|
||||||
|
DayOfWeek::Saturday => Self::Saturday,
|
||||||
|
DayOfWeek::Sunday => Self::Sunday,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct SlotTO {
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub day_of_week: DayOfWeek,
|
||||||
|
pub from: time::Time,
|
||||||
|
pub to: time::Time,
|
||||||
|
pub valid_from: time::Date,
|
||||||
|
pub valid_to: Option<time::Date>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub deleted: Option<time::PrimitiveDateTime>,
|
||||||
|
#[serde(rename = "$version")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub version: Uuid,
|
||||||
|
}
|
||||||
|
impl From<&service::slot::Slot> for SlotTO {
|
||||||
|
fn from(slot: &service::slot::Slot) -> Self {
|
||||||
|
Self {
|
||||||
|
id: slot.id,
|
||||||
|
day_of_week: slot.day_of_week.into(),
|
||||||
|
from: slot.from,
|
||||||
|
to: slot.to,
|
||||||
|
valid_from: slot.valid_from,
|
||||||
|
valid_to: slot.valid_to,
|
||||||
|
deleted: slot.deleted,
|
||||||
|
version: slot.version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<&SlotTO> for service::slot::Slot {
|
||||||
|
fn from(slot: &SlotTO) -> Self {
|
||||||
|
Self {
|
||||||
|
id: slot.id,
|
||||||
|
day_of_week: slot.day_of_week.into(),
|
||||||
|
from: slot.from,
|
||||||
|
to: slot.to,
|
||||||
|
valid_from: slot.valid_from,
|
||||||
|
valid_to: slot.valid_to,
|
||||||
|
deleted: slot.deleted,
|
||||||
|
version: slot.version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_route<RestState: RestStateDef>() -> Router<RestState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(get_all_slots::<RestState>))
|
||||||
|
.route("/:id", get(get_slot::<RestState>))
|
||||||
|
.route("/", post(create_slot::<RestState>))
|
||||||
|
.route("/:id", put(update_slot::<RestState>))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_all_slots<RestState: RestStateDef>(rest_state: State<RestState>) -> Response {
|
||||||
|
error_handler(
|
||||||
|
(async {
|
||||||
|
let slots: Arc<[SlotTO]> = rest_state
|
||||||
|
.slot_service()
|
||||||
|
.get_slots()
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(SlotTO::from)
|
||||||
|
.collect();
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.body(Body::new(serde_json::to_string(&slots).unwrap()))
|
||||||
|
.unwrap())
|
||||||
|
})
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_slot<RestState: RestStateDef>(
|
||||||
|
rest_state: State<RestState>,
|
||||||
|
Path(slot_id): Path<Uuid>,
|
||||||
|
) -> Response {
|
||||||
|
error_handler(
|
||||||
|
(async {
|
||||||
|
let slot = SlotTO::from(&rest_state.slot_service().get_slot(&slot_id).await?.into());
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.body(Body::new(serde_json::to_string(&slot).unwrap()))
|
||||||
|
.unwrap())
|
||||||
|
})
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_slot<RestState: RestStateDef>(
|
||||||
|
rest_state: State<RestState>,
|
||||||
|
Json(slot): Json<SlotTO>,
|
||||||
|
) -> Response {
|
||||||
|
error_handler(
|
||||||
|
(async {
|
||||||
|
let slot = SlotTO::from(
|
||||||
|
&rest_state
|
||||||
|
.slot_service()
|
||||||
|
.create_slot(&(&slot).into())
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.body(Body::new(serde_json::to_string(&slot).unwrap()))
|
||||||
|
.unwrap())
|
||||||
|
})
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_slot<RestState: RestStateDef>(
|
||||||
|
rest_state: State<RestState>,
|
||||||
|
Path(slot_id): Path<Uuid>,
|
||||||
|
Json(slot): Json<SlotTO>,
|
||||||
|
) -> Response {
|
||||||
|
error_handler(
|
||||||
|
(async {
|
||||||
|
if slot_id != slot.id {
|
||||||
|
return Err(RestError::InconsistentId(slot_id, slot.id));
|
||||||
|
}
|
||||||
|
rest_state
|
||||||
|
.slot_service()
|
||||||
|
.update_slot(&(&slot).into())
|
||||||
|
.await?;
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.body(Body::new(serde_json::to_string(&slot).unwrap()))
|
||||||
|
.unwrap())
|
||||||
|
})
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,8 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1.80"
|
async-trait = "0.1.80"
|
||||||
mockall = "0.12.1"
|
mockall = "0.12.1"
|
||||||
|
time = "0.3.36"
|
||||||
|
uuid = "1.8.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
|
|
||||||
8
service/src/clock.rs
Normal file
8
service/src/clock.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
use mockall::automock;
|
||||||
|
|
||||||
|
#[automock]
|
||||||
|
pub trait ClockService {
|
||||||
|
fn time_now(&self) -> time::Time;
|
||||||
|
fn date_now(&self) -> time::Date;
|
||||||
|
fn date_time_now(&self) -> time::PrimitiveDateTime;
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,14 @@ use async_trait::async_trait;
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
use std::{future::Future, sync::Arc};
|
use std::{future::Future, sync::Arc};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use time::Date;
|
||||||
|
use time::Time;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub mod clock;
|
||||||
pub mod permission;
|
pub mod permission;
|
||||||
|
pub mod slot;
|
||||||
|
pub mod uuid_service;
|
||||||
|
|
||||||
pub use permission::MockPermissionService;
|
pub use permission::MockPermissionService;
|
||||||
pub use permission::PermissionService;
|
pub use permission::PermissionService;
|
||||||
|
|
@ -11,6 +17,11 @@ pub use permission::Privilege;
|
||||||
pub use permission::Role;
|
pub use permission::Role;
|
||||||
pub use permission::User;
|
pub use permission::User;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum ValidationFailureItem {
|
||||||
|
ModificationNotAllowed(Arc<str>),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ServiceError {
|
pub enum ServiceError {
|
||||||
#[error("Database query error: {0}")]
|
#[error("Database query error: {0}")]
|
||||||
|
|
@ -18,6 +29,33 @@ pub enum ServiceError {
|
||||||
|
|
||||||
#[error("Forbidden")]
|
#[error("Forbidden")]
|
||||||
Forbidden,
|
Forbidden,
|
||||||
|
|
||||||
|
#[error("Entity {0} aready exists")]
|
||||||
|
EntityAlreadyExists(Uuid),
|
||||||
|
|
||||||
|
#[error("Entity {0} not found")]
|
||||||
|
EntityNotFound(Uuid),
|
||||||
|
|
||||||
|
#[error("Entity {0} conflicts, expected version {1} but got {2}")]
|
||||||
|
EntityConflicts(Uuid, Uuid, Uuid),
|
||||||
|
|
||||||
|
#[error("Validation error: {0:?}")]
|
||||||
|
ValidationError(Arc<[ValidationFailureItem]>),
|
||||||
|
|
||||||
|
#[error("ID cannot be set on create")]
|
||||||
|
IdSetOnCreate,
|
||||||
|
|
||||||
|
#[error("Version cannot be set on create")]
|
||||||
|
VersionSetOnCreate,
|
||||||
|
|
||||||
|
#[error("Overlapping time range")]
|
||||||
|
OverlappingTimeRange,
|
||||||
|
|
||||||
|
#[error("Time order wrong. {0} must is not smaller or equal to {1}")]
|
||||||
|
TimeOrderWrong(Time, Time),
|
||||||
|
|
||||||
|
#[error("Date order wrong. {0} must is not smaller or equal to {1}")]
|
||||||
|
DateOrderWrong(Date, Date),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[automock]
|
#[automock]
|
||||||
|
|
|
||||||
93
service/src/slot.rs
Normal file
93
service/src/slot.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use mockall::automock;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::ServiceError;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum DayOfWeek {
|
||||||
|
Monday,
|
||||||
|
Tuesday,
|
||||||
|
Wednesday,
|
||||||
|
Thursday,
|
||||||
|
Friday,
|
||||||
|
Saturday,
|
||||||
|
Sunday,
|
||||||
|
}
|
||||||
|
impl From<dao::slot::DayOfWeek> for DayOfWeek {
|
||||||
|
fn from(day_of_week: dao::slot::DayOfWeek) -> Self {
|
||||||
|
match day_of_week {
|
||||||
|
dao::slot::DayOfWeek::Monday => Self::Monday,
|
||||||
|
dao::slot::DayOfWeek::Tuesday => Self::Tuesday,
|
||||||
|
dao::slot::DayOfWeek::Wednesday => Self::Wednesday,
|
||||||
|
dao::slot::DayOfWeek::Thursday => Self::Thursday,
|
||||||
|
dao::slot::DayOfWeek::Friday => Self::Friday,
|
||||||
|
dao::slot::DayOfWeek::Saturday => Self::Saturday,
|
||||||
|
dao::slot::DayOfWeek::Sunday => Self::Sunday,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<DayOfWeek> for dao::slot::DayOfWeek {
|
||||||
|
fn from(day_of_week: DayOfWeek) -> Self {
|
||||||
|
match day_of_week {
|
||||||
|
DayOfWeek::Monday => Self::Monday,
|
||||||
|
DayOfWeek::Tuesday => Self::Tuesday,
|
||||||
|
DayOfWeek::Wednesday => Self::Wednesday,
|
||||||
|
DayOfWeek::Thursday => Self::Thursday,
|
||||||
|
DayOfWeek::Friday => Self::Friday,
|
||||||
|
DayOfWeek::Saturday => Self::Saturday,
|
||||||
|
DayOfWeek::Sunday => Self::Sunday,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Slot {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub day_of_week: DayOfWeek,
|
||||||
|
pub from: time::Time,
|
||||||
|
pub to: time::Time,
|
||||||
|
pub valid_from: time::Date,
|
||||||
|
pub valid_to: Option<time::Date>,
|
||||||
|
pub deleted: Option<time::PrimitiveDateTime>,
|
||||||
|
pub version: Uuid,
|
||||||
|
}
|
||||||
|
impl From<&dao::slot::SlotEntity> for Slot {
|
||||||
|
fn from(slot: &dao::slot::SlotEntity) -> Self {
|
||||||
|
Self {
|
||||||
|
id: slot.id,
|
||||||
|
day_of_week: slot.day_of_week.into(),
|
||||||
|
from: slot.from,
|
||||||
|
to: slot.to,
|
||||||
|
valid_from: slot.valid_from,
|
||||||
|
valid_to: slot.valid_to,
|
||||||
|
deleted: slot.deleted,
|
||||||
|
version: slot.version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<&Slot> for dao::slot::SlotEntity {
|
||||||
|
fn from(slot: &Slot) -> Self {
|
||||||
|
Self {
|
||||||
|
id: slot.id,
|
||||||
|
day_of_week: slot.day_of_week.into(),
|
||||||
|
from: slot.from,
|
||||||
|
to: slot.to,
|
||||||
|
valid_from: slot.valid_from,
|
||||||
|
valid_to: slot.valid_to,
|
||||||
|
deleted: slot.deleted,
|
||||||
|
version: slot.version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[automock]
|
||||||
|
#[async_trait]
|
||||||
|
pub trait SlotService {
|
||||||
|
async fn get_slots(&self) -> Result<Arc<[Slot]>, ServiceError>;
|
||||||
|
async fn get_slot(&self, id: &Uuid) -> Result<Slot, ServiceError>;
|
||||||
|
async fn create_slot(&self, slot: &Slot) -> Result<Slot, ServiceError>;
|
||||||
|
async fn delete_slot(&self, id: &Uuid) -> Result<(), ServiceError>;
|
||||||
|
async fn update_slot(&self, slot: &Slot) -> Result<(), ServiceError>;
|
||||||
|
}
|
||||||
7
service/src/uuid_service.rs
Normal file
7
service/src/uuid_service.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
use mockall::automock;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[automock]
|
||||||
|
pub trait UuidService {
|
||||||
|
fn new_uuid(&self, usage: &str) -> Uuid;
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1.80"
|
async-trait = "0.1.80"
|
||||||
mockall = "0.12.1"
|
mockall = "0.12.1"
|
||||||
|
tokio = "1.37.0"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
|
@ -15,6 +16,14 @@ path = "../service"
|
||||||
[dependencies.dao]
|
[dependencies.dao]
|
||||||
path = "../dao"
|
path = "../dao"
|
||||||
|
|
||||||
|
[dependencies.time]
|
||||||
|
version = "0.3.36"
|
||||||
|
features = ["std"]
|
||||||
|
|
||||||
|
[dependencies.uuid]
|
||||||
|
version = "1.8.0"
|
||||||
|
features = ["v4"]
|
||||||
|
|
||||||
[dev-dependencies.tokio]
|
[dev-dependencies.tokio]
|
||||||
version = "1.37.0"
|
version = "1.37.0"
|
||||||
features = ["full"]
|
features = ["full"]
|
||||||
|
|
|
||||||
16
service_impl/src/clock.rs
Normal file
16
service_impl/src/clock.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
use service::clock::ClockService;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
pub struct ClockServiceImpl;
|
||||||
|
impl ClockService for ClockServiceImpl {
|
||||||
|
fn time_now(&self) -> time::Time {
|
||||||
|
OffsetDateTime::now_utc().time()
|
||||||
|
}
|
||||||
|
fn date_now(&self) -> time::Date {
|
||||||
|
OffsetDateTime::now_utc().date()
|
||||||
|
}
|
||||||
|
fn date_time_now(&self) -> time::PrimitiveDateTime {
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
time::PrimitiveDateTime::new(now.date(), now.time())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,11 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
mod permission;
|
pub mod clock;
|
||||||
#[cfg(test)]
|
pub mod permission;
|
||||||
|
pub mod slot;
|
||||||
mod test;
|
mod test;
|
||||||
|
pub mod uuid_service;
|
||||||
|
|
||||||
pub use permission::PermissionServiceImpl;
|
pub use permission::PermissionServiceImpl;
|
||||||
|
|
||||||
|
|
|
||||||
199
service_impl/src/slot.rs
Normal file
199
service_impl/src/slot.rs
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use service::{slot::Slot, ServiceError, ValidationFailureItem};
|
||||||
|
use tokio::join;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
const SLOT_SERVICE_PROCESS: &str = "slot-service";
|
||||||
|
|
||||||
|
pub struct SlotServiceImpl<SlotDao, PermissionService, ClockService, UuidService>
|
||||||
|
where
|
||||||
|
SlotDao: dao::slot::SlotDao + Send + Sync,
|
||||||
|
PermissionService: service::permission::PermissionService + Send + Sync,
|
||||||
|
ClockService: service::clock::ClockService + Send + Sync,
|
||||||
|
UuidService: service::uuid_service::UuidService + Send + Sync,
|
||||||
|
{
|
||||||
|
pub slot_dao: Arc<SlotDao>,
|
||||||
|
pub permission_service: Arc<PermissionService>,
|
||||||
|
pub clock_service: Arc<ClockService>,
|
||||||
|
pub uuid_service: Arc<UuidService>,
|
||||||
|
}
|
||||||
|
impl<SlotDao, PermissionService, ClockService, UuidService>
|
||||||
|
SlotServiceImpl<SlotDao, PermissionService, ClockService, UuidService>
|
||||||
|
where
|
||||||
|
SlotDao: dao::slot::SlotDao + Send + Sync,
|
||||||
|
PermissionService: service::permission::PermissionService + Send + Sync,
|
||||||
|
ClockService: service::clock::ClockService + Send + Sync,
|
||||||
|
UuidService: service::uuid_service::UuidService + Send + Sync,
|
||||||
|
{
|
||||||
|
pub fn new(
|
||||||
|
slot_dao: Arc<SlotDao>,
|
||||||
|
permission_service: Arc<PermissionService>,
|
||||||
|
clock_service: Arc<ClockService>,
|
||||||
|
uuid_service: Arc<UuidService>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
slot_dao,
|
||||||
|
permission_service,
|
||||||
|
clock_service,
|
||||||
|
uuid_service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_overlapping_slots(slot_1: &Slot, slot_2: &Slot) -> bool {
|
||||||
|
slot_1.day_of_week == slot_2.day_of_week
|
||||||
|
&& (slot_2.from < slot_1.from && slot_1.from < slot_2.to
|
||||||
|
|| slot_1.from < slot_2.from && slot_2.from < slot_1.to
|
||||||
|
|| slot_1.from == slot_2.from && slot_1.to == slot_2.to)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<SlotDao, PermissionService, ClockService, UuidService> service::slot::SlotService
|
||||||
|
for SlotServiceImpl<SlotDao, PermissionService, ClockService, UuidService>
|
||||||
|
where
|
||||||
|
SlotDao: dao::slot::SlotDao + Send + Sync,
|
||||||
|
PermissionService: service::permission::PermissionService + Send + Sync,
|
||||||
|
ClockService: service::clock::ClockService + Send + Sync,
|
||||||
|
UuidService: service::uuid_service::UuidService + Send + Sync,
|
||||||
|
{
|
||||||
|
async fn get_slots(&self) -> Result<Arc<[Slot]>, ServiceError> {
|
||||||
|
let (hr_permission, sales_permission) = join!(
|
||||||
|
self.permission_service.check_permission("hr"),
|
||||||
|
self.permission_service.check_permission("sales"),
|
||||||
|
);
|
||||||
|
hr_permission.or(sales_permission)?;
|
||||||
|
|
||||||
|
Ok(self
|
||||||
|
.slot_dao
|
||||||
|
.get_slots()
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(Slot::from)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
async fn get_slot(&self, id: &Uuid) -> Result<Slot, ServiceError> {
|
||||||
|
let (hr_permission, sales_permission) = join!(
|
||||||
|
self.permission_service.check_permission("hr"),
|
||||||
|
self.permission_service.check_permission("sales"),
|
||||||
|
);
|
||||||
|
hr_permission.or(sales_permission)?;
|
||||||
|
|
||||||
|
let slot_entity = self.slot_dao.get_slot(id).await?;
|
||||||
|
let slot = slot_entity
|
||||||
|
.as_ref()
|
||||||
|
.map(Slot::from)
|
||||||
|
.ok_or_else(move || ServiceError::EntityNotFound(*id))?;
|
||||||
|
Ok(slot)
|
||||||
|
}
|
||||||
|
async fn create_slot(&self, slot: &Slot) -> Result<Slot, ServiceError> {
|
||||||
|
self.permission_service.check_permission("hr").await?;
|
||||||
|
|
||||||
|
if slot.id != Uuid::nil() {
|
||||||
|
return Err(ServiceError::IdSetOnCreate);
|
||||||
|
}
|
||||||
|
if slot.version != Uuid::nil() {
|
||||||
|
return Err(ServiceError::VersionSetOnCreate);
|
||||||
|
}
|
||||||
|
if slot.from > slot.to {
|
||||||
|
return Err(ServiceError::TimeOrderWrong(slot.from, slot.to));
|
||||||
|
}
|
||||||
|
if slot.valid_to.is_some() && slot.valid_to.unwrap() < slot.valid_from {
|
||||||
|
return Err(ServiceError::DateOrderWrong(
|
||||||
|
slot.valid_from,
|
||||||
|
slot.valid_to.unwrap(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self
|
||||||
|
.get_slots()
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.any(|s| test_overlapping_slots(slot, s))
|
||||||
|
{
|
||||||
|
return Err(ServiceError::OverlappingTimeRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
let slot = Slot {
|
||||||
|
id: self.uuid_service.new_uuid("slot-id"),
|
||||||
|
version: self.uuid_service.new_uuid("slot-version"),
|
||||||
|
..slot.clone()
|
||||||
|
};
|
||||||
|
self.slot_dao
|
||||||
|
.create_slot(&(&slot).into(), SLOT_SERVICE_PROCESS)
|
||||||
|
.await?;
|
||||||
|
Ok(slot)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_slot(&self, id: &Uuid) -> Result<(), ServiceError> {
|
||||||
|
self.permission_service.check_permission("hr").await?;
|
||||||
|
let mut slot = self
|
||||||
|
.slot_dao
|
||||||
|
.get_slot(id)
|
||||||
|
.await?
|
||||||
|
.ok_or(ServiceError::EntityNotFound(*id))?;
|
||||||
|
slot.deleted = Some(self.clock_service.date_time_now());
|
||||||
|
self.slot_dao
|
||||||
|
.update_slot(&slot, SLOT_SERVICE_PROCESS)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn update_slot(&self, slot: &Slot) -> Result<(), ServiceError> {
|
||||||
|
self.permission_service.check_permission("hr").await?;
|
||||||
|
let persisted_slot = self
|
||||||
|
.slot_dao
|
||||||
|
.get_slot(&slot.id)
|
||||||
|
.await?
|
||||||
|
.ok_or(ServiceError::EntityNotFound(slot.id))?;
|
||||||
|
if persisted_slot.version != slot.version {
|
||||||
|
return Err(ServiceError::EntityConflicts(
|
||||||
|
slot.id,
|
||||||
|
persisted_slot.version,
|
||||||
|
slot.version,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if slot.valid_to.is_some() && slot.valid_to.unwrap() < slot.valid_from {
|
||||||
|
return Err(ServiceError::DateOrderWrong(
|
||||||
|
slot.valid_from,
|
||||||
|
slot.valid_to.unwrap(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut validation = Vec::new();
|
||||||
|
if persisted_slot.day_of_week != slot.day_of_week.into() {
|
||||||
|
validation.push(ValidationFailureItem::ModificationNotAllowed(
|
||||||
|
"day_of_week".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if persisted_slot.from != slot.from {
|
||||||
|
validation.push(ValidationFailureItem::ModificationNotAllowed("from".into()));
|
||||||
|
}
|
||||||
|
if persisted_slot.to != slot.to {
|
||||||
|
validation.push(ValidationFailureItem::ModificationNotAllowed("to".into()));
|
||||||
|
}
|
||||||
|
if persisted_slot.valid_from != slot.valid_from {
|
||||||
|
validation.push(ValidationFailureItem::ModificationNotAllowed(
|
||||||
|
"valid_from".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if persisted_slot.valid_to.is_some() && persisted_slot.valid_to != slot.valid_to {
|
||||||
|
validation.push(ValidationFailureItem::ModificationNotAllowed(
|
||||||
|
"valid_to".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validation.is_empty() {
|
||||||
|
return Err(ServiceError::ValidationError(validation.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let slot = Slot {
|
||||||
|
version: self.uuid_service.new_uuid("slot-version"),
|
||||||
|
..slot.clone()
|
||||||
|
};
|
||||||
|
self.slot_dao
|
||||||
|
.update_slot(&(&slot).into(), SLOT_SERVICE_PROCESS)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +1,4 @@
|
||||||
|
#[cfg(test)]
|
||||||
mod permission_test;
|
mod permission_test;
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod slot;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ fn generate_dependencies_mocks_permission(
|
||||||
(permission_dao, user_service)
|
(permission_dao, user_service)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_forbidden<T>(result: &Result<T, service::ServiceError>) {
|
pub fn test_forbidden<T>(result: &Result<T, service::ServiceError>) {
|
||||||
if let Err(service::ServiceError::Forbidden) = result {
|
if let Err(service::ServiceError::Forbidden) = result {
|
||||||
// All good
|
// All good
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
924
service_impl/src/test/slot.rs
Normal file
924
service_impl/src/test/slot.rs
Normal file
|
|
@ -0,0 +1,924 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::slot::*;
|
||||||
|
use crate::test::permission_test::test_forbidden;
|
||||||
|
use dao::slot::{MockSlotDao, SlotEntity};
|
||||||
|
use mockall::predicate::eq;
|
||||||
|
use service::{
|
||||||
|
clock::MockClockService, slot::*, uuid_service::MockUuidService, MockPermissionService,
|
||||||
|
ValidationFailureItem,
|
||||||
|
};
|
||||||
|
use time::{Date, Month, PrimitiveDateTime, Time};
|
||||||
|
use tokio;
|
||||||
|
use uuid::{uuid, Uuid};
|
||||||
|
|
||||||
|
pub fn default_id() -> Uuid {
|
||||||
|
uuid!("682DA62E-20CB-49D9-A2A7-3F53C6842405")
|
||||||
|
}
|
||||||
|
pub fn default_version() -> Uuid {
|
||||||
|
uuid!("86DE856C-D176-4F1F-A4FE-0D9844C02C03")
|
||||||
|
}
|
||||||
|
pub fn default_changed_version() -> Uuid {
|
||||||
|
uuid!("4A818852-45D2-400F-A02A-755D34FFE815")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_default_slot() -> Slot {
|
||||||
|
Slot {
|
||||||
|
id: default_id(),
|
||||||
|
day_of_week: DayOfWeek::Monday,
|
||||||
|
from: time::Time::from_hms(10, 0, 0).unwrap(),
|
||||||
|
to: time::Time::from_hms(11, 0, 0).unwrap(),
|
||||||
|
valid_from: time::Date::from_calendar_date(2022, 1.try_into().unwrap(), 1).unwrap(),
|
||||||
|
valid_to: None,
|
||||||
|
deleted: None,
|
||||||
|
version: default_version(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn generate_default_slot_entity() -> SlotEntity {
|
||||||
|
SlotEntity {
|
||||||
|
id: uuid!("682DA62E-20CB-49D9-A2A7-3F53C6842405"),
|
||||||
|
day_of_week: dao::slot::DayOfWeek::Monday,
|
||||||
|
from: time::Time::from_hms(10, 0, 0).unwrap(),
|
||||||
|
to: time::Time::from_hms(11, 0, 0).unwrap(),
|
||||||
|
valid_from: time::Date::from_calendar_date(2022, 1.try_into().unwrap(), 1).unwrap(),
|
||||||
|
valid_to: None,
|
||||||
|
deleted: None,
|
||||||
|
version: uuid!("86DE856C-D176-4F1F-A4FE-0D9844C02C03"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_not_found<T>(result: &Result<T, service::ServiceError>, target_id: &Uuid) {
|
||||||
|
if let Err(service::ServiceError::EntityNotFound(id)) = result {
|
||||||
|
assert_eq!(
|
||||||
|
id, target_id,
|
||||||
|
"Expected entity {} not found but got {}",
|
||||||
|
target_id, id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("Expected entity {} not found error", target_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_zero_id_error<T>(result: &Result<T, service::ServiceError>) {
|
||||||
|
if let Err(service::ServiceError::IdSetOnCreate) = result {
|
||||||
|
} else {
|
||||||
|
panic!("Expected id set on create error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_zero_version_error<T>(result: &Result<T, service::ServiceError>) {
|
||||||
|
if let Err(service::ServiceError::VersionSetOnCreate) = result {
|
||||||
|
} else {
|
||||||
|
panic!("Expected version set on create error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_overlapping_time_range_error<T>(result: &Result<T, service::ServiceError>) {
|
||||||
|
if let Err(service::ServiceError::OverlappingTimeRange) = result {
|
||||||
|
} else {
|
||||||
|
panic!("Expected overlapping time range error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_time_order_wrong<T>(result: &Result<T, service::ServiceError>) {
|
||||||
|
if let Err(service::ServiceError::TimeOrderWrong(_from, _to)) = result {
|
||||||
|
} else {
|
||||||
|
panic!("Expected time order failure");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_date_order_wrong<T>(result: &Result<T, service::ServiceError>) {
|
||||||
|
if let Err(service::ServiceError::DateOrderWrong(_from, _to)) = result {
|
||||||
|
} else {
|
||||||
|
panic!("Expected date order failure");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_conflicts<T>(
|
||||||
|
result: &Result<T, service::ServiceError>,
|
||||||
|
target_id: &Uuid,
|
||||||
|
expected_version: &Uuid,
|
||||||
|
actual_version: &Uuid,
|
||||||
|
) {
|
||||||
|
if let Err(service::ServiceError::EntityConflicts(
|
||||||
|
err_id,
|
||||||
|
err_expected_version,
|
||||||
|
err_actual_version,
|
||||||
|
)) = result
|
||||||
|
{
|
||||||
|
assert_eq!(
|
||||||
|
err_id, target_id,
|
||||||
|
"Expected entity {} conflicts but got {}",
|
||||||
|
target_id, err_id
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
expected_version, err_expected_version,
|
||||||
|
"Expected expected version {} but got {}",
|
||||||
|
expected_version, err_expected_version
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
actual_version, err_actual_version,
|
||||||
|
"Expected actual version {} but got {}",
|
||||||
|
actual_version, err_actual_version
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("Expected entity {} conflicts error", target_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_validation_error(
|
||||||
|
result: &Result<(), service::ServiceError>,
|
||||||
|
validation_failure: &ValidationFailureItem,
|
||||||
|
fail_count: usize,
|
||||||
|
) {
|
||||||
|
if let Err(service::ServiceError::ValidationError(validation_failure_items)) = result {
|
||||||
|
if !validation_failure_items.contains(validation_failure) {
|
||||||
|
panic!(
|
||||||
|
"Validation failure not found: {:?} in {:?}",
|
||||||
|
validation_failure, validation_failure_items
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(fail_count, validation_failure_items.len());
|
||||||
|
} else {
|
||||||
|
panic!("Expected validation error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SlotServiceDependencies {
|
||||||
|
pub slot_dao: MockSlotDao,
|
||||||
|
pub permission_service: MockPermissionService,
|
||||||
|
pub clock_service: MockClockService,
|
||||||
|
pub uuid_service: MockUuidService,
|
||||||
|
}
|
||||||
|
impl SlotServiceDependencies {
|
||||||
|
pub fn build_service(
|
||||||
|
self,
|
||||||
|
) -> SlotServiceImpl<MockSlotDao, MockPermissionService, MockClockService, MockUuidService>
|
||||||
|
{
|
||||||
|
SlotServiceImpl::new(
|
||||||
|
self.slot_dao.into(),
|
||||||
|
self.permission_service.into(),
|
||||||
|
self.clock_service.into(),
|
||||||
|
self.uuid_service.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_dependencies(permission: bool, role: &'static str) -> SlotServiceDependencies {
|
||||||
|
let slot_dao = MockSlotDao::new();
|
||||||
|
let mut permission_service = MockPermissionService::new();
|
||||||
|
permission_service
|
||||||
|
.expect_check_permission()
|
||||||
|
.with(eq(role))
|
||||||
|
.returning(move |_| {
|
||||||
|
if permission {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(service::ServiceError::Forbidden)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
permission_service
|
||||||
|
.expect_check_permission()
|
||||||
|
.returning(move |_| Err(service::ServiceError::Forbidden));
|
||||||
|
let mut clock_service = MockClockService::new();
|
||||||
|
clock_service
|
||||||
|
.expect_time_now()
|
||||||
|
.returning(|| time::Time::from_hms(23, 42, 0).unwrap());
|
||||||
|
clock_service
|
||||||
|
.expect_date_now()
|
||||||
|
.returning(|| time::Date::from_calendar_date(2063, 4.try_into().unwrap(), 5).unwrap());
|
||||||
|
clock_service.expect_date_time_now().returning(|| {
|
||||||
|
time::PrimitiveDateTime::new(
|
||||||
|
time::Date::from_calendar_date(2063, 4.try_into().unwrap(), 5).unwrap(),
|
||||||
|
time::Time::from_hms(23, 42, 0).unwrap(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let uuid_service = MockUuidService::new();
|
||||||
|
|
||||||
|
SlotServiceDependencies {
|
||||||
|
slot_dao,
|
||||||
|
permission_service,
|
||||||
|
clock_service,
|
||||||
|
uuid_service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_slots() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies.slot_dao.expect_get_slots().returning(|| {
|
||||||
|
Ok(Arc::new([
|
||||||
|
SlotEntity {
|
||||||
|
id: uuid!("DA703BC1-F488-4E4F-BA10-0972196639F7"),
|
||||||
|
version: uuid!("FAC4FAD9-89AE-4E56-9608-03C56558B192"),
|
||||||
|
..generate_default_slot_entity()
|
||||||
|
},
|
||||||
|
generate_default_slot_entity(),
|
||||||
|
]))
|
||||||
|
});
|
||||||
|
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
|
||||||
|
let result = slot_service.get_slots().await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let result = result.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.len(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
result[0],
|
||||||
|
Slot {
|
||||||
|
id: uuid!("DA703BC1-F488-4E4F-BA10-0972196639F7"),
|
||||||
|
version: uuid!("FAC4FAD9-89AE-4E56-9608-03C56558B192"),
|
||||||
|
..generate_default_slot()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(result[1], generate_default_slot(),);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_slots_sales_role() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slots()
|
||||||
|
.returning(|| Ok(Arc::new([])));
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service.get_slots().await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_slots_no_permission() {
|
||||||
|
let mut dependencies = build_dependencies(false, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slots()
|
||||||
|
.returning(|| Ok(Arc::new([])));
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service.get_slots().await;
|
||||||
|
test_forbidden(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_slot() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slot()
|
||||||
|
.with(eq(default_id()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| Ok(Some(generate_default_slot_entity())));
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service.get_slot(&default_id()).await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let result = result.unwrap();
|
||||||
|
assert_eq!(result, generate_default_slot());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_slot_sales_role() {
|
||||||
|
let mut dependencies = build_dependencies(true, "sales");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slot()
|
||||||
|
.with(eq(default_id()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| Ok(Some(generate_default_slot_entity())));
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service.get_slot(&default_id()).await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_slot_not_found() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slot()
|
||||||
|
.with(eq(default_id()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| Ok(None));
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service.get_slot(&default_id()).await;
|
||||||
|
test_not_found(&result, &default_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_slot_no_permission() {
|
||||||
|
let dependencies = build_dependencies(false, "hr");
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service.get_slot(&default_id()).await;
|
||||||
|
test_forbidden(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_slot() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_create_slot()
|
||||||
|
.with(eq(generate_default_slot_entity()), eq("slot-service"))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_, _| Ok(()));
|
||||||
|
dependencies
|
||||||
|
.uuid_service
|
||||||
|
.expect_new_uuid()
|
||||||
|
.with(eq("slot-id"))
|
||||||
|
.returning(|_| default_id());
|
||||||
|
dependencies
|
||||||
|
.uuid_service
|
||||||
|
.expect_new_uuid()
|
||||||
|
.with(eq("slot-version"))
|
||||||
|
.returning(|_| default_version());
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slots()
|
||||||
|
.returning(|| Ok(Arc::new([])));
|
||||||
|
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service
|
||||||
|
.create_slot(&Slot {
|
||||||
|
id: Uuid::nil(),
|
||||||
|
version: Uuid::nil(),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(result.unwrap(), generate_default_slot());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_slot_no_permission() {
|
||||||
|
let dependencies = build_dependencies(false, "hr");
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service.create_slot(&generate_default_slot()).await;
|
||||||
|
test_forbidden(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_slot_non_zero_id() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.uuid_service
|
||||||
|
.expect_new_uuid()
|
||||||
|
.with(eq("slot-id"))
|
||||||
|
.returning(|_| default_id());
|
||||||
|
dependencies
|
||||||
|
.uuid_service
|
||||||
|
.expect_new_uuid()
|
||||||
|
.with(eq("slot-version"))
|
||||||
|
.returning(|_| default_version());
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service
|
||||||
|
.create_slot(&Slot {
|
||||||
|
version: Uuid::nil(),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_zero_id_error(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_slot_non_zero_version() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.uuid_service
|
||||||
|
.expect_new_uuid()
|
||||||
|
.with(eq("slot-id"))
|
||||||
|
.returning(|_| default_id());
|
||||||
|
dependencies
|
||||||
|
.uuid_service
|
||||||
|
.expect_new_uuid()
|
||||||
|
.with(eq("slot-version"))
|
||||||
|
.returning(|_| default_version());
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service
|
||||||
|
.create_slot(&Slot {
|
||||||
|
id: Uuid::nil(),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_zero_version_error(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_slot_intersects() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies.slot_dao.expect_get_slots().returning(|| {
|
||||||
|
Ok(Arc::new([
|
||||||
|
generate_default_slot_entity(),
|
||||||
|
SlotEntity {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
from: Time::from_hms(12, 0, 0).unwrap(),
|
||||||
|
to: Time::from_hms(13, 0, 0).unwrap(),
|
||||||
|
..generate_default_slot_entity()
|
||||||
|
},
|
||||||
|
SlotEntity {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
day_of_week: DayOfWeek::Wednesday.into(),
|
||||||
|
from: Time::from_hms(11, 0, 0).unwrap(),
|
||||||
|
to: Time::from_hms(12, 0, 0).unwrap(),
|
||||||
|
..generate_default_slot_entity()
|
||||||
|
},
|
||||||
|
]))
|
||||||
|
});
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_create_slot()
|
||||||
|
.returning(|_, _| Ok(()));
|
||||||
|
dependencies
|
||||||
|
.uuid_service
|
||||||
|
.expect_new_uuid()
|
||||||
|
.with(eq("slot-id"))
|
||||||
|
.returning(|_| default_id());
|
||||||
|
dependencies
|
||||||
|
.uuid_service
|
||||||
|
.expect_new_uuid()
|
||||||
|
.with(eq("slot-version"))
|
||||||
|
.returning(|_| default_version());
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
|
||||||
|
// Test successful case, directly between two existing slots.
|
||||||
|
let result = slot_service
|
||||||
|
.create_slot(&Slot {
|
||||||
|
id: Uuid::nil(),
|
||||||
|
version: Uuid::nil(),
|
||||||
|
from: Time::from_hms(11, 0, 0).unwrap(),
|
||||||
|
to: Time::from_hms(12, 0, 0).unwrap(),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
// Test case where it is exactly on an existing slot.
|
||||||
|
let result = slot_service
|
||||||
|
.create_slot(&Slot {
|
||||||
|
id: Uuid::nil(),
|
||||||
|
version: Uuid::nil(),
|
||||||
|
from: Time::from_hms(10, 0, 0).unwrap(),
|
||||||
|
to: Time::from_hms(11, 0, 0).unwrap(),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_overlapping_time_range_error(&result);
|
||||||
|
|
||||||
|
// Test case where from is inside an existing slot.
|
||||||
|
let result = slot_service
|
||||||
|
.create_slot(&Slot {
|
||||||
|
id: Uuid::nil(),
|
||||||
|
version: Uuid::nil(),
|
||||||
|
from: Time::from_hms(10, 30, 0).unwrap(),
|
||||||
|
to: Time::from_hms(11, 30, 0).unwrap(),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_overlapping_time_range_error(&result);
|
||||||
|
|
||||||
|
// Test case where to is inside an existing slot.
|
||||||
|
let result = slot_service
|
||||||
|
.create_slot(&Slot {
|
||||||
|
id: Uuid::nil(),
|
||||||
|
version: Uuid::nil(),
|
||||||
|
from: Time::from_hms(11, 30, 0).unwrap(),
|
||||||
|
to: Time::from_hms(12, 30, 0).unwrap(),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_overlapping_time_range_error(&result);
|
||||||
|
|
||||||
|
// Test case where is completely inside an existing slot.
|
||||||
|
let result = slot_service
|
||||||
|
.create_slot(&Slot {
|
||||||
|
id: Uuid::nil(),
|
||||||
|
version: Uuid::nil(),
|
||||||
|
from: Time::from_hms(10, 15, 0).unwrap(),
|
||||||
|
to: Time::from_hms(10, 45, 0).unwrap(),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_overlapping_time_range_error(&result);
|
||||||
|
|
||||||
|
// Test case where is completely outside of an existing slot.
|
||||||
|
let result = slot_service
|
||||||
|
.create_slot(&Slot {
|
||||||
|
id: Uuid::nil(),
|
||||||
|
version: Uuid::nil(),
|
||||||
|
from: Time::from_hms(9, 0, 0).unwrap(),
|
||||||
|
to: Time::from_hms(11, 0, 0).unwrap(),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_overlapping_time_range_error(&result);
|
||||||
|
|
||||||
|
// Test case where is would intersect on monday but not on tuesday.
|
||||||
|
// Test case where is completely outside of an existing slot.
|
||||||
|
let result = slot_service
|
||||||
|
.create_slot(&Slot {
|
||||||
|
id: Uuid::nil(),
|
||||||
|
version: Uuid::nil(),
|
||||||
|
day_of_week: DayOfWeek::Tuesday.into(),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_slot_time_order() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_create_slot()
|
||||||
|
.returning(|_, _| Ok(()));
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slots()
|
||||||
|
.returning(|| Ok(Arc::new([])));
|
||||||
|
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service
|
||||||
|
.create_slot(&Slot {
|
||||||
|
id: Uuid::nil(),
|
||||||
|
version: Uuid::nil(),
|
||||||
|
from: Time::from_hms(12, 00, 0).unwrap(),
|
||||||
|
to: Time::from_hms(11, 00, 00).unwrap(),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_time_order_wrong(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_slot_date_order() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_create_slot()
|
||||||
|
.returning(|_, _| Ok(()));
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slots()
|
||||||
|
.returning(|| Ok(Arc::new([])));
|
||||||
|
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service
|
||||||
|
.create_slot(&Slot {
|
||||||
|
id: Uuid::nil(),
|
||||||
|
version: Uuid::nil(),
|
||||||
|
valid_from: Date::from_calendar_date(2022, Month::January, 2).unwrap(),
|
||||||
|
valid_to: Some(Date::from_calendar_date(2022, Month::January, 1).unwrap()),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_date_order_wrong(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_slot() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slot()
|
||||||
|
.with(eq(default_id()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| Ok(Some(generate_default_slot_entity())));
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_update_slot()
|
||||||
|
.with(
|
||||||
|
eq(SlotEntity {
|
||||||
|
deleted: Some(PrimitiveDateTime::new(
|
||||||
|
Date::from_calendar_date(2063, time::Month::April, 5).unwrap(),
|
||||||
|
Time::from_hms(23, 42, 0).unwrap(),
|
||||||
|
)),
|
||||||
|
..generate_default_slot_entity()
|
||||||
|
}),
|
||||||
|
eq("slot-service"),
|
||||||
|
)
|
||||||
|
.times(1)
|
||||||
|
.returning(|_, _| Ok(()));
|
||||||
|
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service.delete_slot(&default_id()).await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_slot_no_permission() {
|
||||||
|
let dependencies = build_dependencies(false, "hr");
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service.delete_slot(&default_id()).await;
|
||||||
|
test_forbidden(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_slot_not_found() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slot()
|
||||||
|
.with(eq(default_id()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| Ok(None));
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service.delete_slot(&default_id()).await;
|
||||||
|
test_not_found(&result, &default_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_slot_no_permission() {
|
||||||
|
let dependencies = build_dependencies(false, "hr");
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service.update_slot(&generate_default_slot()).await;
|
||||||
|
test_forbidden(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_slot_not_found() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slot()
|
||||||
|
.with(eq(default_id()))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| Ok(None));
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service.update_slot(&generate_default_slot()).await;
|
||||||
|
test_not_found(&result, &default_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_slot_version_mismatch() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slot()
|
||||||
|
.with(eq(default_id()))
|
||||||
|
.returning(|_| Ok(Some(generate_default_slot_entity())));
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service
|
||||||
|
.update_slot(&service::slot::Slot {
|
||||||
|
version: uuid!("86DE856C-D176-4F1F-A4FE-0D9844C02C04"),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_conflicts(
|
||||||
|
&result,
|
||||||
|
&default_id(),
|
||||||
|
&default_version(),
|
||||||
|
&uuid!("86DE856C-D176-4F1F-A4FE-0D9844C02C04"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_slot_valid_to() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_update_slot()
|
||||||
|
.once()
|
||||||
|
.with(
|
||||||
|
eq(dao::slot::SlotEntity {
|
||||||
|
valid_to: Some(
|
||||||
|
time::Date::from_calendar_date(2022, 1.try_into().unwrap(), 10).unwrap(),
|
||||||
|
),
|
||||||
|
version: default_changed_version(),
|
||||||
|
..generate_default_slot_entity()
|
||||||
|
}),
|
||||||
|
eq("slot-service"),
|
||||||
|
)
|
||||||
|
.times(1)
|
||||||
|
.returning(|_, _| Ok(()));
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slot()
|
||||||
|
.with(eq(default_id()))
|
||||||
|
.returning(|_| Ok(Some(generate_default_slot_entity())));
|
||||||
|
dependencies
|
||||||
|
.uuid_service
|
||||||
|
.expect_new_uuid()
|
||||||
|
.with(eq("slot-version"))
|
||||||
|
.returning(|_| default_changed_version());
|
||||||
|
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service
|
||||||
|
.update_slot(&service::slot::Slot {
|
||||||
|
valid_to: Some(
|
||||||
|
time::Date::from_calendar_date(2022, 1.try_into().unwrap(), 10).unwrap(),
|
||||||
|
),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
dbg!(&result);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_slot_valid_to_before_valid_from() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slot()
|
||||||
|
.with(eq(default_id()))
|
||||||
|
.returning(|_| Ok(Some(generate_default_slot_entity())));
|
||||||
|
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service
|
||||||
|
.update_slot(&service::slot::Slot {
|
||||||
|
valid_to: Some(
|
||||||
|
time::Date::from_calendar_date(2021, 1.try_into().unwrap(), 10).unwrap(),
|
||||||
|
),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_date_order_wrong(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_slot_deleted() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slot()
|
||||||
|
.with(eq(default_id()))
|
||||||
|
.returning(|_| Ok(Some(generate_default_slot_entity())));
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_update_slot()
|
||||||
|
.once()
|
||||||
|
.with(
|
||||||
|
eq(dao::slot::SlotEntity {
|
||||||
|
deleted: Some(time::PrimitiveDateTime::new(
|
||||||
|
Date::from_calendar_date(2022, 1.try_into().unwrap(), 10).unwrap(),
|
||||||
|
Time::from_hms(0, 0, 0).unwrap(),
|
||||||
|
)),
|
||||||
|
version: default_changed_version(),
|
||||||
|
..generate_default_slot_entity()
|
||||||
|
}),
|
||||||
|
eq("slot-service"),
|
||||||
|
)
|
||||||
|
.returning(|_, _| Ok(()));
|
||||||
|
dependencies
|
||||||
|
.uuid_service
|
||||||
|
.expect_new_uuid()
|
||||||
|
.with(eq("slot-version"))
|
||||||
|
.returning(|_| default_changed_version());
|
||||||
|
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service
|
||||||
|
.update_slot(&Slot {
|
||||||
|
deleted: Some(time::PrimitiveDateTime::new(
|
||||||
|
Date::from_calendar_date(2022, 1.try_into().unwrap(), 10).unwrap(),
|
||||||
|
Time::from_hms(0, 0, 0).unwrap(),
|
||||||
|
)),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_slot_day_of_week_forbidden() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slot()
|
||||||
|
.with(eq(default_id()))
|
||||||
|
.returning(|_| Ok(Some(generate_default_slot_entity())));
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service
|
||||||
|
.update_slot(&service::slot::Slot {
|
||||||
|
day_of_week: service::slot::DayOfWeek::Friday,
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_validation_error(
|
||||||
|
&result,
|
||||||
|
&ValidationFailureItem::ModificationNotAllowed("day_of_week".into()),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_to_forbidden_when_not_none() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slot()
|
||||||
|
.with(eq(default_id()))
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(Some(SlotEntity {
|
||||||
|
valid_to: Some(
|
||||||
|
time::Date::from_calendar_date(2022, 1.try_into().unwrap(), 3).unwrap(),
|
||||||
|
),
|
||||||
|
..generate_default_slot_entity()
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service
|
||||||
|
.update_slot(&service::slot::Slot {
|
||||||
|
valid_to: Some(time::Date::from_calendar_date(2022, 1.try_into().unwrap(), 4).unwrap()),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_validation_error(
|
||||||
|
&result,
|
||||||
|
&ValidationFailureItem::ModificationNotAllowed("valid_to".into()),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_from_forbidden() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slot()
|
||||||
|
.with(eq(default_id()))
|
||||||
|
.returning(|_| Ok(Some(generate_default_slot_entity())));
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service
|
||||||
|
.update_slot(&service::slot::Slot {
|
||||||
|
from: time::Time::from_hms(14, 0, 0).unwrap(),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_validation_error(
|
||||||
|
&result,
|
||||||
|
&ValidationFailureItem::ModificationNotAllowed("from".into()),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_to_forbidden() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slot()
|
||||||
|
.with(eq(default_id()))
|
||||||
|
.returning(|_| Ok(Some(generate_default_slot_entity())));
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service
|
||||||
|
.update_slot(&service::slot::Slot {
|
||||||
|
to: time::Time::from_hms(14, 0, 0).unwrap(),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_validation_error(
|
||||||
|
&result,
|
||||||
|
&ValidationFailureItem::ModificationNotAllowed("to".into()),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_valid_from_forbidden() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slot()
|
||||||
|
.with(eq(default_id()))
|
||||||
|
.returning(|_| Ok(Some(generate_default_slot_entity())));
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service
|
||||||
|
.update_slot(&service::slot::Slot {
|
||||||
|
valid_from: time::Date::from_calendar_date(2022, 1.try_into().unwrap(), 10).unwrap(),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_validation_error(
|
||||||
|
&result,
|
||||||
|
&ValidationFailureItem::ModificationNotAllowed("valid_from".into()),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_valid_multiple_forbidden_changes() {
|
||||||
|
let mut dependencies = build_dependencies(true, "hr");
|
||||||
|
dependencies
|
||||||
|
.slot_dao
|
||||||
|
.expect_get_slot()
|
||||||
|
.with(eq(default_id()))
|
||||||
|
.returning(|_| Ok(Some(generate_default_slot_entity())));
|
||||||
|
let slot_service = dependencies.build_service();
|
||||||
|
let result = slot_service
|
||||||
|
.update_slot(&service::slot::Slot {
|
||||||
|
valid_from: time::Date::from_calendar_date(2022, 1.try_into().unwrap(), 10).unwrap(),
|
||||||
|
from: time::Time::from_hms(14, 0, 0).unwrap(),
|
||||||
|
..generate_default_slot()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
test_validation_error(
|
||||||
|
&result,
|
||||||
|
&ValidationFailureItem::ModificationNotAllowed("valid_from".into()),
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
test_validation_error(
|
||||||
|
&result,
|
||||||
|
&ValidationFailureItem::ModificationNotAllowed("from".into()),
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
9
service_impl/src/uuid_service.rs
Normal file
9
service_impl/src/uuid_service.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub struct UuidServiceImpl;
|
||||||
|
|
||||||
|
impl service::uuid_service::UuidService for UuidServiceImpl {
|
||||||
|
fn new_uuid(&self, _usage: &str) -> Uuid {
|
||||||
|
Uuid::new_v4()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue