Commons & STP
This commit is contained in:
parent
47084819e0
commit
958cf8829a
9 changed files with 1188 additions and 0 deletions
262
types/src/todo.rs
Normal file
262
types/src/todo.rs
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::enums::ToDoStatus;
|
||||
use crate::errors::ValidationError;
|
||||
use crate::{CommunicationType, CommunicationValue, DataTypes, DataValue};
|
||||
|
||||
const MAX_TITLE_LENGTH: usize = 500;
|
||||
const MAX_DESCRIPTION_LENGTH: usize = 10000;
|
||||
const MIN_PRIORITY: u32 = 1;
|
||||
const MAX_PRIORITY: u32 = 1000;
|
||||
const DEFAULT_PRIORITY: u32 = 100;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ToDo {
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub status: ToDoStatus,
|
||||
pub priority: u32,
|
||||
pub depends_on: Vec<Uuid>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub created_by: Uuid,
|
||||
pub project_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl Default for ToDo {
|
||||
fn default() -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
title: String::new(),
|
||||
description: String::new(),
|
||||
status: ToDoStatus::default(),
|
||||
priority: DEFAULT_PRIORITY,
|
||||
depends_on: Vec::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created_by: Uuid::nil(),
|
||||
project_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToDo {
|
||||
pub fn new(title: String, description: String, created_by: Uuid) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
title: title.trim().to_string(),
|
||||
description,
|
||||
status: ToDoStatus::default(),
|
||||
priority: DEFAULT_PRIORITY,
|
||||
depends_on: Vec::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created_by,
|
||||
project_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_project(mut self, project_id: Uuid) -> Self {
|
||||
self.project_id = Some(project_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), ValidationError> {
|
||||
if self.title.is_empty() {
|
||||
return Err(ValidationError::EmptyTitle);
|
||||
}
|
||||
|
||||
if self.title.len() > MAX_TITLE_LENGTH {
|
||||
return Err(ValidationError::TitleTooLong);
|
||||
}
|
||||
|
||||
if self.description.len() > MAX_DESCRIPTION_LENGTH {
|
||||
return Err(ValidationError::DescriptionTooLong);
|
||||
}
|
||||
|
||||
if self.priority < MIN_PRIORITY || self.priority > MAX_PRIORITY {
|
||||
return Err(ValidationError::InvalidPriority);
|
||||
}
|
||||
|
||||
if self.has_circular_dependency() {
|
||||
return Err(ValidationError::CircularDependency(self.id.to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn has_circular_dependency(&self) -> bool {
|
||||
if self.depends_on.is_empty() {
|
||||
return false;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, status: ToDoStatus) -> Result<(), ValidationError> {
|
||||
let valid_transition = match (&self.status, &status) {
|
||||
(ToDoStatus::Pending, ToDoStatus::InProgress) => true,
|
||||
(ToDoStatus::Pending, ToDoStatus::Blocked) => true,
|
||||
(ToDoStatus::InProgress, ToDoStatus::Completed) => true,
|
||||
(ToDoStatus::InProgress, ToDoStatus::Blocked) => true,
|
||||
(ToDoStatus::Blocked, ToDoStatus::InProgress) => true,
|
||||
(ToDoStatus::Blocked, ToDoStatus::Pending) => true,
|
||||
(ToDoStatus::Completed, ToDoStatus::InProgress) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if !valid_transition {
|
||||
return Err(ValidationError::InvalidStatusTransition {
|
||||
from: self.status.to_string(),
|
||||
to: status.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
priority: Option<u32>,
|
||||
) -> Result<(), ValidationError> {
|
||||
if let Some(t) = title {
|
||||
let trimmed = t.trim().to_string();
|
||||
if trimmed.is_empty() {
|
||||
return Err(ValidationError::EmptyTitle);
|
||||
}
|
||||
if trimmed.len() > MAX_TITLE_LENGTH {
|
||||
return Err(ValidationError::TitleTooLong);
|
||||
}
|
||||
self.title = trimmed;
|
||||
}
|
||||
|
||||
if let Some(d) = description {
|
||||
if d.len() > MAX_DESCRIPTION_LENGTH {
|
||||
return Err(ValidationError::DescriptionTooLong);
|
||||
}
|
||||
self.description = d;
|
||||
}
|
||||
|
||||
if let Some(p) = priority {
|
||||
if p < MIN_PRIORITY || p > MAX_PRIORITY {
|
||||
return Err(ValidationError::InvalidPriority);
|
||||
}
|
||||
self.priority = p;
|
||||
}
|
||||
|
||||
self.updated_at = Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ToDo> for CommunicationValue {
|
||||
fn from(todo: ToDo) -> Self {
|
||||
let mut cv = CommunicationValue::new(CommunicationType::todo);
|
||||
cv = cv.add_data(DataTypes::id, DataValue::Str(todo.id.to_string()));
|
||||
cv = cv.add_data(DataTypes::title, DataValue::Str(todo.title));
|
||||
cv = cv.add_data(DataTypes::description, DataValue::Str(todo.description));
|
||||
cv = cv.add_data(DataTypes::status, DataValue::Str(todo.status.to_string()));
|
||||
cv = cv.add_data(DataTypes::todo_id, DataValue::Number(todo.priority as i64));
|
||||
|
||||
let depends: Vec<DataValue> = todo
|
||||
.depends_on
|
||||
.iter()
|
||||
.map(|u| DataValue::Str(u.to_string()))
|
||||
.collect();
|
||||
cv = cv.add_data(DataTypes::depends_on, DataValue::Array(depends));
|
||||
|
||||
cv = cv.add_data(
|
||||
DataTypes::created_at,
|
||||
DataValue::Str(todo.created_at.to_rfc3339()),
|
||||
);
|
||||
cv = cv.add_data(
|
||||
DataTypes::updated_at,
|
||||
DataValue::Str(todo.updated_at.to_rfc3339()),
|
||||
);
|
||||
cv = cv.add_data(
|
||||
DataTypes::user_id,
|
||||
DataValue::Str(todo.created_by.to_string()),
|
||||
);
|
||||
|
||||
if let Some(pid) = todo.project_id {
|
||||
cv = cv.add_data(DataTypes::project_id, DataValue::Str(pid.to_string()));
|
||||
}
|
||||
|
||||
cv
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_todo_new() {
|
||||
let todo = ToDo::new(
|
||||
"Test Title".to_string(),
|
||||
"Test Description".to_string(),
|
||||
Uuid::new_v4(),
|
||||
);
|
||||
assert!(!todo.id.is_nil());
|
||||
assert_eq!(todo.title, "Test Title");
|
||||
assert_eq!(todo.description, "Test Description");
|
||||
assert_eq!(todo.status, ToDoStatus::Pending);
|
||||
assert_eq!(todo.priority, DEFAULT_PRIORITY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_todo_validate_empty_title() {
|
||||
let mut todo = ToDo::default();
|
||||
todo.title = "".to_string();
|
||||
assert!(matches!(todo.validate(), Err(ValidationError::EmptyTitle)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_todo_validate_title_too_long() {
|
||||
let mut todo = ToDo::default();
|
||||
todo.title = "a".repeat(MAX_TITLE_LENGTH + 1);
|
||||
assert!(matches!(
|
||||
todo.validate(),
|
||||
Err(ValidationError::TitleTooLong)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_todo_validate_priority() {
|
||||
let mut todo = ToDo::new("Test".to_string(), "desc".to_string(), Uuid::new_v4());
|
||||
todo.priority = 0;
|
||||
assert!(matches!(
|
||||
todo.validate(),
|
||||
Err(ValidationError::InvalidPriority)
|
||||
));
|
||||
|
||||
let mut todo = ToDo::new("Test".to_string(), "desc".to_string(), Uuid::new_v4());
|
||||
todo.priority = 1001;
|
||||
assert!(matches!(
|
||||
todo.validate(),
|
||||
Err(ValidationError::InvalidPriority)
|
||||
));
|
||||
|
||||
let mut todo = ToDo::new("Test".to_string(), "desc".to_string(), Uuid::new_v4());
|
||||
todo.priority = 500;
|
||||
assert!(todo.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_todo_status_transition() {
|
||||
let mut todo = ToDo::default();
|
||||
|
||||
assert!(todo.set_status(ToDoStatus::InProgress).is_ok());
|
||||
assert!(matches!(
|
||||
todo.set_status(ToDoStatus::Pending),
|
||||
Err(ValidationError::InvalidStatusTransition { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue