use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; 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, Serialize, Deserialize)] pub struct ToDo { pub id: Uuid, pub title: String, pub description: String, pub status: ToDoStatus, pub priority: u32, pub depends_on: Vec, pub created_at: DateTime, pub updated_at: DateTime, pub created_by: Uuid, pub project_id: Option, } 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, description: Option, priority: Option, ) -> 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 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 = 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 { .. }) )); } }