use crate::model::{TimeLog, TrackableItem};
use chrono::{Local, NaiveDate};
use std::collections::HashSet;
#[derive(Debug)]
pub enum ValidationProblem {
ExcessiveHours { max_hours: u16 },
MissingSummary,
FutureDate,
DuplicateEntry,
BeforeStartDate { start_date: NaiveDate },
}
pub struct ValidationResult<'a> {
pub time_log: &'a TimeLog,
pub problems: Vec<ValidationProblem>,
}
impl ValidationResult<'_> {
#[must_use]
pub fn is_valid(&self) -> bool {
self.problems.is_empty()
}
#[must_use]
pub fn has_problems(&self, problem_type: &ValidationProblem) -> bool {
self.problems
.iter()
.any(|i| std::mem::discriminant(i) == std::mem::discriminant(problem_type))
}
}
pub trait Validator {
fn validate_single(&mut self, time_log: &TimeLog) -> Vec<ValidationProblem>;
}
pub struct TimeLogValidator {
validators: Vec<Box<dyn Validator>>,
}
impl Default for TimeLogValidator {
fn default() -> Self {
Self::new()
}
}
impl TimeLogValidator {
#[must_use]
pub fn new() -> Self {
Self {
validators: Vec::new(),
}
}
#[must_use]
pub fn with_validator(mut self, validator: impl Validator + 'static) -> Self {
self.validators.push(Box::new(validator));
self
}
pub fn validate<'a>(&mut self, time_logs: &'a [TimeLog]) -> Vec<ValidationResult<'a>> {
let validation_results: Vec<ValidationResult<'a>> = time_logs
.iter()
.map(|time_log| {
let mut problems = Vec::new();
for validator in &mut self.validators {
problems.extend(validator.validate_single(time_log));
}
ValidationResult { time_log, problems }
})
.collect();
validation_results
}
}
pub struct ExcessiveHoursValidator {
max_hours: u16,
}
impl ExcessiveHoursValidator {
#[must_use]
pub fn new(max_hours: u16) -> Self {
Self { max_hours }
}
}
impl Validator for ExcessiveHoursValidator {
fn validate_single(&mut self, time_log: &TimeLog) -> Vec<ValidationProblem> {
let hours = time_log.time_spent.num_hours();
match hours > i64::from(self.max_hours) {
true => vec![ValidationProblem::ExcessiveHours {
max_hours: self.max_hours,
}],
false => Vec::new(),
}
}
}
pub struct HasSummaryValidator;
impl Validator for HasSummaryValidator {
fn validate_single(&mut self, time_log: &TimeLog) -> Vec<ValidationProblem> {
match time_log.summary.is_none() {
true => vec![ValidationProblem::MissingSummary],
false => Vec::new(),
}
}
}
pub struct NoFutureDateValidator;
impl Validator for NoFutureDateValidator {
fn validate_single(&mut self, time_log: &TimeLog) -> Vec<ValidationProblem> {
if time_log.spent_at > Local::now() {
return vec![ValidationProblem::FutureDate];
}
Vec::new()
}
}
pub struct DuplicatesValidator {
seen: HashSet<(String, NaiveDate, i64, Option<String>, TrackableItem)>,
}
impl Default for DuplicatesValidator {
fn default() -> Self {
Self::new()
}
}
impl DuplicatesValidator {
#[must_use]
pub fn new() -> Self {
Self {
seen: HashSet::new(),
}
}
}
impl Validator for DuplicatesValidator {
fn validate_single(&mut self, time_log: &TimeLog) -> Vec<ValidationProblem> {
let key = (
time_log.user.name.clone(),
time_log.spent_at.date_naive(),
time_log.time_spent.num_seconds(),
time_log.summary.clone(),
time_log.trackable_item.clone(),
);
if self.seen.insert(key) {
Vec::new()
} else {
vec![ValidationProblem::DuplicateEntry]
}
}
}
pub struct BeforeStartDateValidator {
start_date: NaiveDate,
}
impl BeforeStartDateValidator {
#[must_use]
pub fn new(start_date: NaiveDate) -> Self {
Self { start_date }
}
}
impl Validator for BeforeStartDateValidator {
fn validate_single(&mut self, time_log: &TimeLog) -> Vec<ValidationProblem> {
let log_date = time_log.spent_at.date_naive();
if log_date < self.start_date {
return vec![ValidationProblem::BeforeStartDate {
start_date: self.start_date,
}];
}
Vec::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{MergeRequest, TimeLog, TrackableItemFields, TrackableItemKind};
use chrono::{Duration, Local, TimeDelta};
const NUMBER_OF_LOGS: usize = 7;
fn get_time_logs() -> [TimeLog; NUMBER_OF_LOGS] {
[
TimeLog {
summary: Some("Valid Time log".to_string()),
spent_at: Local::now() - TimeDelta::days(1),
time_spent: Duration::hours(4),
trackable_item: TrackableItem {
common: TrackableItemFields {
title: "test".to_string(),
..Default::default()
},
kind: TrackableItemKind::MergeRequest(MergeRequest::default()),
},
..Default::default()
},
TimeLog {
summary: Some("Excessive Hours".to_string()),
spent_at: Local::now() - TimeDelta::days(1),
time_spent: Duration::hours(12),
..Default::default()
},
TimeLog {
summary: None,
spent_at: Local::now() - TimeDelta::days(1),
time_spent: Duration::hours(5) + Duration::minutes(30),
..Default::default()
},
TimeLog {
summary: Some("Future Date".to_string()),
spent_at: Local::now() + TimeDelta::hours(1),
time_spent: Duration::hours(3),
..Default::default()
},
TimeLog {
summary: Some("Valid Time log".to_string()),
spent_at: Local::now() - TimeDelta::days(1),
time_spent: Duration::hours(4),
trackable_item: TrackableItem {
common: TrackableItemFields {
title: "test".to_string(),
..Default::default()
},
kind: TrackableItemKind::MergeRequest(MergeRequest::default()),
},
..Default::default()
},
TimeLog {
summary: None,
spent_at: Local::now() + TimeDelta::days(1),
time_spent: Duration::hours(15),
..Default::default()
},
TimeLog {
summary: Some("Same time spent & spent_at as 'Summary missing' timelog, but different summary".to_string()),
spent_at: Local::now() - TimeDelta::days(1),
time_spent: Duration::hours(5) + Duration::minutes(30),
..Default::default()
}
]
}
#[test]
fn test_excessive_hours_validator() {
const EXCESSIVE_HOURS_LIMIT: u16 = 10;
let time_logs = get_time_logs();
let expected_problem = ValidationProblem::ExcessiveHours {
max_hours: EXCESSIVE_HOURS_LIMIT,
};
let mut validator = TimeLogValidator::new()
.with_validator(ExcessiveHoursValidator::new(EXCESSIVE_HOURS_LIMIT));
let results = validator.validate(&time_logs);
assert_eq!(results.len(), NUMBER_OF_LOGS);
for (i, result) in results.iter().enumerate() {
match i {
1 | 5 => assert!(result.has_problems(&expected_problem)),
_ => assert!(result.is_valid()),
}
}
}
#[test]
fn test_has_summary_validator() {
let time_logs = get_time_logs();
let expected_problem = ValidationProblem::MissingSummary;
let mut validator = TimeLogValidator::new().with_validator(HasSummaryValidator);
let results = validator.validate(&time_logs);
assert_eq!(results.len(), NUMBER_OF_LOGS);
for (i, result) in results.iter().enumerate() {
match i {
2 | 5 => assert!(result.has_problems(&expected_problem)),
_ => assert!(result.is_valid()),
}
}
}
#[test]
fn test_future_date_validator() {
let time_logs = get_time_logs();
let expected_problem = ValidationProblem::FutureDate;
let mut validator = TimeLogValidator::new().with_validator(NoFutureDateValidator);
let results = validator.validate(&time_logs);
assert_eq!(results.len(), NUMBER_OF_LOGS);
for (i, result) in results.iter().enumerate() {
match i {
3 | 5 => assert!(result.has_problems(&expected_problem)),
_ => assert!(result.is_valid()),
}
}
}
#[test]
fn test_duplicates_validator() {
let time_logs = get_time_logs();
let expected_problem = ValidationProblem::DuplicateEntry;
let mut validator = TimeLogValidator::new().with_validator(DuplicatesValidator::new());
let results = validator.validate(&time_logs);
assert_eq!(results.len(), NUMBER_OF_LOGS);
for (i, result) in results.iter().enumerate() {
match i {
4 => assert!(result.has_problems(&expected_problem)),
_ => assert!(result.is_valid()),
}
}
}
#[test]
fn test_before_start_date_validator() {
let time_logs = get_time_logs();
let start_date = Local::now().date_naive();
let expected_problem = ValidationProblem::BeforeStartDate { start_date };
let mut validator =
TimeLogValidator::new().with_validator(BeforeStartDateValidator::new(start_date));
let results = validator.validate(&time_logs);
assert_eq!(results.len(), NUMBER_OF_LOGS);
for (i, result) in results.iter().enumerate() {
match i {
0 | 1 | 2 | 4 | 6 => assert!(result.has_problems(&expected_problem)),
_ => assert!(result.is_valid()),
}
}
}
#[test]
fn test_all_validators() {
const EXCESSIVE_HOURS_LIMIT: u16 = 10;
let time_logs = get_time_logs();
let mut validator = TimeLogValidator::new()
.with_validator(ExcessiveHoursValidator::new(EXCESSIVE_HOURS_LIMIT))
.with_validator(HasSummaryValidator)
.with_validator(NoFutureDateValidator)
.with_validator(DuplicatesValidator::new());
let excessive_hours_validator = ValidationProblem::ExcessiveHours {
max_hours: EXCESSIVE_HOURS_LIMIT,
};
let results = validator.validate(&time_logs);
assert_eq!(results.len(), NUMBER_OF_LOGS);
assert!(results[0].is_valid());
assert_eq!(results[1].problems.len(), 1);
assert!(results[1].has_problems(&excessive_hours_validator));
assert_eq!(results[2].problems.len(), 1);
assert!(results[2].has_problems(&ValidationProblem::MissingSummary));
assert_eq!(results[3].problems.len(), 1);
assert!(results[3].has_problems(&ValidationProblem::FutureDate));
assert_eq!(results[4].problems.len(), 1);
assert!(results[4].has_problems(&ValidationProblem::DuplicateEntry));
assert_eq!(results[5].problems.len(), 3);
assert!(results[5].has_problems(&excessive_hours_validator));
assert!(results[5].has_problems(&ValidationProblem::MissingSummary));
assert!(results[5].has_problems(&ValidationProblem::FutureDate));
assert!(results[6].is_valid());
}
}