structure
This commit is contained in:
parent
9fbcee0bea
commit
ae7ec3ea8a
13 changed files with 991 additions and 19 deletions
138
src/coral.rs
Normal file
138
src/coral.rs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::krill::KrillId;
|
||||
|
||||
pub type CoralId = Uuid;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Coral {
|
||||
pub id: CoralId,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub krill_ids: Vec<KrillId>,
|
||||
pub capabilities: Vec<String>,
|
||||
pub score: f64,
|
||||
pub total_runs: u32,
|
||||
pub successful_runs: u32,
|
||||
}
|
||||
|
||||
impl Default for Coral {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
name: String::new(),
|
||||
description: String::new(),
|
||||
krill_ids: Vec::new(),
|
||||
capabilities: Vec::new(),
|
||||
score: 0.0,
|
||||
total_runs: 0,
|
||||
successful_runs: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Coral {
|
||||
pub fn new(name: String, description: String) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
name,
|
||||
description,
|
||||
krill_ids: Vec::new(),
|
||||
capabilities: Vec::new(),
|
||||
score: 0.0,
|
||||
total_runs: 0,
|
||||
successful_runs: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_krill(&mut self, krill_id: KrillId, capability: String) {
|
||||
if !self.krill_ids.contains(&krill_id) {
|
||||
self.krill_ids.push(krill_id);
|
||||
}
|
||||
if !self.capabilities.contains(&capability) {
|
||||
self.capabilities.push(capability);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_score(&self) -> f64 {
|
||||
if self.total_runs == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.successful_runs as f64 / self.total_runs as f64
|
||||
}
|
||||
|
||||
pub fn add_run(&mut self, success: bool) {
|
||||
self.total_runs += 1;
|
||||
if success {
|
||||
self.successful_runs += 1;
|
||||
}
|
||||
self.score = self.calculate_score();
|
||||
}
|
||||
|
||||
pub fn has_capability(&self, capability: &str) -> bool {
|
||||
self.capabilities
|
||||
.iter()
|
||||
.any(|c| c.eq_ignore_ascii_case(capability))
|
||||
}
|
||||
|
||||
pub fn can_handle(&self, required_capabilities: &[String]) -> bool {
|
||||
required_capabilities
|
||||
.iter()
|
||||
.all(|cap| self.has_capability(cap))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_coral_new() {
|
||||
let coral = Coral::new("Test Coral".to_string(), "A test coral".to_string());
|
||||
assert!(!coral.id.is_nil());
|
||||
assert_eq!(coral.name, "Test Coral");
|
||||
assert_eq!(coral.score, 0.0);
|
||||
assert_eq!(coral.total_runs, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coral_add_krill() {
|
||||
let mut coral = Coral::default();
|
||||
coral.add_krill(Uuid::new_v4(), "coding".to_string());
|
||||
coral.add_krill(Uuid::new_v4(), "analysis".to_string());
|
||||
|
||||
assert_eq!(coral.krill_ids.len(), 2);
|
||||
assert_eq!(coral.capabilities.len(), 2);
|
||||
assert!(coral.has_capability("coding"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coral_add_run() {
|
||||
let mut coral = Coral::default();
|
||||
|
||||
coral.add_run(true);
|
||||
assert_eq!(coral.total_runs, 1);
|
||||
assert_eq!(coral.successful_runs, 1);
|
||||
assert_eq!(coral.score, 1.0);
|
||||
|
||||
coral.add_run(false);
|
||||
assert_eq!(coral.total_runs, 2);
|
||||
assert_eq!(coral.successful_runs, 1);
|
||||
assert_eq!(coral.score, 0.5);
|
||||
|
||||
coral.add_run(true);
|
||||
coral.add_run(true);
|
||||
assert_eq!(coral.score, 0.75);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coral_can_handle() {
|
||||
let mut coral = Coral::default();
|
||||
coral.add_krill(Uuid::new_v4(), "coding".to_string());
|
||||
coral.add_krill(Uuid::new_v4(), "analysis".to_string());
|
||||
|
||||
assert!(coral.can_handle(&["coding".to_string()]));
|
||||
assert!(coral.can_handle(&["coding".to_string(), "analysis".to_string()]));
|
||||
assert!(!coral.can_handle(&["coding".to_string(), "vision".to_string()]));
|
||||
}
|
||||
}
|
||||
190
src/enums.rs
Normal file
190
src/enums.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ToDoStatus {
|
||||
Pending,
|
||||
InProgress,
|
||||
Completed,
|
||||
Blocked,
|
||||
}
|
||||
|
||||
impl Default for ToDoStatus {
|
||||
fn default() -> Self {
|
||||
ToDoStatus::Pending
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ToDoStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ToDoStatus::Pending => write!(f, "pending"),
|
||||
ToDoStatus::InProgress => write!(f, "in_progress"),
|
||||
ToDoStatus::Completed => write!(f, "completed"),
|
||||
ToDoStatus::Blocked => write!(f, "blocked"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for ToDoStatus {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"pending" => Ok(ToDoStatus::Pending),
|
||||
"in_progress" => Ok(ToDoStatus::InProgress),
|
||||
"completed" => Ok(ToDoStatus::Completed),
|
||||
"blocked" => Ok(ToDoStatus::Blocked),
|
||||
_ => Err(format!("unknown status: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TaskStatus {
|
||||
Pending,
|
||||
Running,
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl Default for TaskStatus {
|
||||
fn default() -> Self {
|
||||
TaskStatus::Pending
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TaskStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
TaskStatus::Pending => write!(f, "pending"),
|
||||
TaskStatus::Running => write!(f, "running"),
|
||||
TaskStatus::Completed => write!(f, "completed"),
|
||||
TaskStatus::Failed => write!(f, "failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for TaskStatus {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"pending" => Ok(TaskStatus::Pending),
|
||||
"running" => Ok(TaskStatus::Running),
|
||||
"completed" => Ok(TaskStatus::Completed),
|
||||
"failed" => Ok(TaskStatus::Failed),
|
||||
_ => Err(format!("unknown status: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ModelType {
|
||||
Llm,
|
||||
CodeGen,
|
||||
Vision,
|
||||
Audio,
|
||||
Text,
|
||||
Embedding,
|
||||
}
|
||||
|
||||
impl fmt::Display for ModelType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ModelType::Llm => write!(f, "llm"),
|
||||
ModelType::CodeGen => write!(f, "codegen"),
|
||||
ModelType::Vision => write!(f, "vision"),
|
||||
ModelType::Audio => write!(f, "audio"),
|
||||
ModelType::Text => write!(f, "text"),
|
||||
ModelType::Embedding => write!(f, "embedding"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for ModelType {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"llm" => Ok(ModelType::Llm),
|
||||
"codegen" => Ok(ModelType::CodeGen),
|
||||
"vision" => Ok(ModelType::Vision),
|
||||
"audio" => Ok(ModelType::Audio),
|
||||
"text" => Ok(ModelType::Text),
|
||||
"embedding" => Ok(ModelType::Embedding),
|
||||
_ => Err(format!("unknown model type: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DocType {
|
||||
ApiDoc,
|
||||
Readme,
|
||||
CodeComment,
|
||||
Architecture,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for DocType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
DocType::ApiDoc => write!(f, "api_doc"),
|
||||
DocType::Readme => write!(f, "readme"),
|
||||
DocType::CodeComment => write!(f, "code_comment"),
|
||||
DocType::Architecture => write!(f, "architecture"),
|
||||
DocType::Other(s) => write!(f, "other:{}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PromptType {
|
||||
Authority,
|
||||
Confirm,
|
||||
Select,
|
||||
}
|
||||
|
||||
impl fmt::Display for PromptType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
PromptType::Authority => write!(f, "authority"),
|
||||
PromptType::Confirm => write!(f, "confirm"),
|
||||
PromptType::Select => write!(f, "select"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PromptOption {
|
||||
Accept,
|
||||
Deny,
|
||||
AcceptAlways,
|
||||
Forbid,
|
||||
Yes,
|
||||
No,
|
||||
OptionA,
|
||||
OptionB,
|
||||
OptionC,
|
||||
}
|
||||
|
||||
impl fmt::Display for PromptOption {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
PromptOption::Accept => write!(f, "accept"),
|
||||
PromptOption::Deny => write!(f, "deny"),
|
||||
PromptOption::AcceptAlways => write!(f, "accept_always"),
|
||||
PromptOption::Forbid => write!(f, "forbid"),
|
||||
PromptOption::Yes => write!(f, "yes"),
|
||||
PromptOption::No => write!(f, "no"),
|
||||
PromptOption::OptionA => write!(f, "option_a"),
|
||||
PromptOption::OptionB => write!(f, "option_b"),
|
||||
PromptOption::OptionC => write!(f, "option_c"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TaskResultType {
|
||||
Success,
|
||||
Error,
|
||||
Split,
|
||||
}
|
||||
45
src/errors.rs
Normal file
45
src/errors.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum ValidationError {
|
||||
#[error("title cannot be empty")]
|
||||
EmptyTitle,
|
||||
#[error("title exceeds maximum length of 500 characters")]
|
||||
TitleTooLong,
|
||||
#[error("description exceeds maximum length of 10000 characters")]
|
||||
DescriptionTooLong,
|
||||
#[error("priority must be between 1 and 1000")]
|
||||
InvalidPriority,
|
||||
#[error("circular dependency detected: {0}")]
|
||||
CircularDependency(String),
|
||||
#[error("dependency not found: {0}")]
|
||||
DependencyNotFound(String),
|
||||
#[error("invalid status transition from {from} to {to}")]
|
||||
InvalidStatusTransition { from: String, to: String },
|
||||
#[error("invalid field: {0}")]
|
||||
InvalidField(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum ShoalError {
|
||||
#[error("storage error: {0}")]
|
||||
StorageError(String),
|
||||
#[error("network error: {0}")]
|
||||
NetworkError(String),
|
||||
#[error("validation error: {0}")]
|
||||
ValidationError(String),
|
||||
#[error("not found: {0}")]
|
||||
NotFound(String),
|
||||
#[error("unauthorized: {0}")]
|
||||
Unauthorized(String),
|
||||
#[error("database error: {0}")]
|
||||
DatabaseError(String),
|
||||
#[error("internal error: {0}")]
|
||||
InternalError(String),
|
||||
}
|
||||
|
||||
impl From<ValidationError> for ShoalError {
|
||||
fn from(e: ValidationError) -> Self {
|
||||
ShoalError::ValidationError(e.to_string())
|
||||
}
|
||||
}
|
||||
133
src/krill.rs
Normal file
133
src/krill.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::enums::ModelType;
|
||||
|
||||
pub type KrillId = Uuid;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KrillDescriptor {
|
||||
pub id: KrillId,
|
||||
pub name: String,
|
||||
pub capabilities: Vec<String>,
|
||||
pub model_type: ModelType,
|
||||
pub model_path: Option<String>,
|
||||
pub config: KrillConfig,
|
||||
pub version: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Default for KrillDescriptor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
name: String::new(),
|
||||
capabilities: Vec::new(),
|
||||
model_type: ModelType::Llm,
|
||||
model_path: None,
|
||||
config: KrillConfig::default(),
|
||||
version: "0.1.0".to_string(),
|
||||
created_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KrillDescriptor {
|
||||
pub fn new(name: String, model_type: ModelType) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
name,
|
||||
capabilities: Vec::new(),
|
||||
model_type,
|
||||
model_path: None,
|
||||
config: KrillConfig::default(),
|
||||
version: "0.1.0".to_string(),
|
||||
created_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_capabilities(mut self, capabilities: Vec<String>) -> Self {
|
||||
self.capabilities = capabilities;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_model_path(mut self, path: String) -> Self {
|
||||
self.model_path = Some(path);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_config(mut self, config: KrillConfig) -> Self {
|
||||
self.config = config;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_version(mut self, version: String) -> Self {
|
||||
self.version = version;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_capability(&self, capability: &str) -> bool {
|
||||
self.capabilities
|
||||
.iter()
|
||||
.any(|c| c.eq_ignore_ascii_case(capability))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct KrillConfig {
|
||||
pub settings: serde_json::Value,
|
||||
}
|
||||
|
||||
impl KrillConfig {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
settings: serde_json::json!({}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_setting<T: serde::Serialize>(mut self, key: &str, value: T) -> Self {
|
||||
if let Ok(value) = serde_json::to_value(value) {
|
||||
if let Some(obj) = self.settings.as_object_mut() {
|
||||
obj.insert(key.to_string(), value);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
|
||||
self.settings.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_krill_descriptor_new() {
|
||||
let krill = KrillDescriptor::new("Test Krill".to_string(), ModelType::CodeGen);
|
||||
assert!(!krill.id.is_nil());
|
||||
assert_eq!(krill.name, "Test Krill");
|
||||
assert_eq!(krill.model_type, ModelType::CodeGen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_krill_descriptor_with_capabilities() {
|
||||
let krill = KrillDescriptor::new("Test".to_string(), ModelType::Llm)
|
||||
.with_capabilities(vec!["coding".to_string(), "analysis".to_string()]);
|
||||
|
||||
assert!(krill.has_capability("coding"));
|
||||
assert!(krill.has_capability("analysis"));
|
||||
assert!(!krill.has_capability("vision"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_krill_config() {
|
||||
let config = KrillConfig::new()
|
||||
.with_setting("temperature", 0.7)
|
||||
.with_setting("max_tokens", 2048);
|
||||
|
||||
assert_eq!(config.get("temperature").unwrap().as_f64(), Some(0.7));
|
||||
assert_eq!(config.get("max_tokens").unwrap().as_u64(), Some(2048));
|
||||
}
|
||||
}
|
||||
19
src/lib.rs
Normal file
19
src/lib.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
pub use chrono::{DateTime, Utc};
|
||||
pub use stp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue};
|
||||
pub use uuid::Uuid;
|
||||
|
||||
pub mod coral;
|
||||
pub mod enums;
|
||||
pub mod errors;
|
||||
pub mod krill;
|
||||
pub mod project;
|
||||
pub mod task;
|
||||
pub mod todo;
|
||||
|
||||
pub use coral::{Coral, CoralId};
|
||||
pub use enums::*;
|
||||
pub use errors::{ShoalError, ValidationError};
|
||||
pub use krill::{KrillConfig, KrillDescriptor, KrillId};
|
||||
pub use project::{Project, ProjectFile, ProjectId, ProjectSettings};
|
||||
pub use task::{Task, TaskResult};
|
||||
pub use todo::ToDo;
|
||||
195
src/project.rs
Normal file
195
src/project.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub type ProjectId = Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Project {
|
||||
pub id: ProjectId,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub files: Vec<ProjectFile>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub owner_id: Uuid,
|
||||
pub settings: ProjectSettings,
|
||||
}
|
||||
|
||||
impl Default for Project {
|
||||
fn default() -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
name: String::new(),
|
||||
description: None,
|
||||
files: Vec::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
owner_id: Uuid::nil(),
|
||||
settings: ProjectSettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn new(name: String, owner_id: Uuid) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
name: name.trim().to_string(),
|
||||
description: None,
|
||||
files: Vec::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
owner_id,
|
||||
settings: ProjectSettings::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_description(mut self, description: String) -> Self {
|
||||
self.description = Some(description);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_settings(mut self, settings: ProjectSettings) -> Self {
|
||||
self.settings = settings;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_file(&mut self, file: ProjectFile) {
|
||||
self.files.push(file);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn remove_file(&mut self, file_id: Uuid) -> Option<ProjectFile> {
|
||||
if let Some(pos) = self.files.iter().position(|f| f.id == file_id) {
|
||||
self.updated_at = Utc::now();
|
||||
Some(self.files.remove(pos))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProjectFile {
|
||||
pub id: Uuid,
|
||||
pub path: String,
|
||||
pub hash: String,
|
||||
pub size: u64,
|
||||
pub mime_type: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl ProjectFile {
|
||||
pub fn new(path: String, hash: String, size: u64) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
path,
|
||||
hash,
|
||||
size,
|
||||
mime_type: None,
|
||||
created_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_mime_type(mut self, mime_type: String) -> Self {
|
||||
self.mime_type = Some(mime_type);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_valid_size(&self) -> bool {
|
||||
const MAX_SIZE: u64 = 1_073_741_824; // 1GB
|
||||
self.size <= MAX_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ProjectSettings {
|
||||
pub language: Option<String>,
|
||||
pub build_command: Option<String>,
|
||||
pub run_command: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub extra: serde_json::Value,
|
||||
}
|
||||
|
||||
impl ProjectSettings {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
language: None,
|
||||
build_command: None,
|
||||
run_command: None,
|
||||
extra: serde_json::json!({}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_language(mut self, language: String) -> Self {
|
||||
self.language = Some(language);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_build_command(mut self, command: String) -> Self {
|
||||
self.build_command = Some(command);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_run_command(mut self, command: String) -> Self {
|
||||
self.run_command = Some(command);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_extra(&mut self, key: &str, value: serde_json::Value) {
|
||||
if let Some(obj) = self.extra.as_object_mut() {
|
||||
obj.insert(key.to_string(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_project_new() {
|
||||
let owner_id = Uuid::new_v4();
|
||||
let project = Project::new("Test Project".to_string(), owner_id);
|
||||
assert!(!project.id.is_nil());
|
||||
assert_eq!(project.name, "Test Project");
|
||||
assert_eq!(project.owner_id, owner_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_with_description() {
|
||||
let project = Project::new("Test".to_string(), Uuid::new_v4())
|
||||
.with_description("A test project".to_string());
|
||||
assert_eq!(project.description, Some("A test project".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_file() {
|
||||
let file = ProjectFile::new("src/main.rs".to_string(), "abc123".to_string(), 1024);
|
||||
assert!(!file.id.is_nil());
|
||||
assert_eq!(file.path, "src/main.rs");
|
||||
assert!(file.is_valid_size());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_file_size_limit() {
|
||||
let large_file =
|
||||
ProjectFile::new("large.bin".to_string(), "hash".to_string(), 2_000_000_000);
|
||||
assert!(!large_file.is_valid_size());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_settings() {
|
||||
let settings = ProjectSettings::new()
|
||||
.with_language("rust".to_string())
|
||||
.with_build_command("cargo build".to_string())
|
||||
.with_run_command("cargo run".to_string());
|
||||
|
||||
assert_eq!(settings.language, Some("rust".to_string()));
|
||||
assert_eq!(settings.build_command, Some("cargo build".to_string()));
|
||||
assert_eq!(settings.run_command, Some("cargo run".to_string()));
|
||||
}
|
||||
}
|
||||
230
src/task.rs
Normal file
230
src/task.rs
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::enums::{TaskResultType, TaskStatus};
|
||||
use crate::todo::ToDo;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Task {
|
||||
pub id: Uuid,
|
||||
pub todo_id: Uuid,
|
||||
pub description: String,
|
||||
pub status: TaskStatus,
|
||||
pub assigned_krill: Option<Uuid>,
|
||||
pub assigned_pod: Option<Uuid>,
|
||||
pub result: Option<TaskResult>,
|
||||
pub logs: Vec<String>,
|
||||
pub started_at: Option<DateTime<Utc>>,
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl Default for Task {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
todo_id: Uuid::nil(),
|
||||
description: String::new(),
|
||||
status: TaskStatus::default(),
|
||||
assigned_krill: None,
|
||||
assigned_pod: None,
|
||||
result: None,
|
||||
logs: Vec::new(),
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Task {
|
||||
pub fn new(todo_id: Uuid, description: String) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
todo_id,
|
||||
description,
|
||||
status: TaskStatus::default(),
|
||||
assigned_krill: None,
|
||||
assigned_pod: None,
|
||||
result: None,
|
||||
logs: Vec::new(),
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self) -> Result<(), String> {
|
||||
match self.status {
|
||||
TaskStatus::Pending => {
|
||||
self.status = TaskStatus::Running;
|
||||
self.started_at = Some(Utc::now());
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(format!("cannot start task from status: {:?}", self.status)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn complete(&mut self, result: TaskResult) -> Result<(), String> {
|
||||
match self.status {
|
||||
TaskStatus::Running => {
|
||||
self.status = TaskStatus::Completed;
|
||||
self.completed_at = Some(Utc::now());
|
||||
self.result = Some(result);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(format!(
|
||||
"cannot complete task from status: {:?}",
|
||||
self.status
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fail(&mut self, error: String) -> Result<(), String> {
|
||||
match self.status {
|
||||
TaskStatus::Running => {
|
||||
self.status = TaskStatus::Failed;
|
||||
self.completed_at = Some(Utc::now());
|
||||
self.result = Some(TaskResult::Error(error));
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(format!("cannot fail task from status: {:?}", self.status)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn retry(&mut self) -> Result<(), String> {
|
||||
match self.status {
|
||||
TaskStatus::Failed | TaskStatus::Pending => {
|
||||
self.status = TaskStatus::Pending;
|
||||
self.started_at = None;
|
||||
self.completed_at = None;
|
||||
self.result = None;
|
||||
self.logs.clear();
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(format!("cannot retry task from status: {:?}", self.status)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn duration_ms(&self) -> Option<i64> {
|
||||
match (self.started_at, self.completed_at) {
|
||||
(Some(start), Some(end)) => Some((end - start).num_milliseconds()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_log(&mut self, log: String) {
|
||||
self.logs.push(log);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum TaskResult {
|
||||
Success(String),
|
||||
Error(String),
|
||||
Split(Vec<ToDo>),
|
||||
}
|
||||
|
||||
impl TaskResult {
|
||||
pub fn result_type(&self) -> TaskResultType {
|
||||
match self {
|
||||
TaskResult::Success(_) => TaskResultType::Success,
|
||||
TaskResult::Error(_) => TaskResultType::Error,
|
||||
TaskResult::Split(_) => TaskResultType::Split,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_task_new() {
|
||||
let todo_id = Uuid::new_v4();
|
||||
let task = Task::new(todo_id, "Test task description".to_string());
|
||||
assert!(!task.id.is_nil());
|
||||
assert_eq!(task.todo_id, todo_id);
|
||||
assert_eq!(task.description, "Test task description");
|
||||
assert_eq!(task.status, TaskStatus::Pending);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_start() {
|
||||
let mut task = Task::default();
|
||||
assert!(task.start().is_ok());
|
||||
assert_eq!(task.status, TaskStatus::Running);
|
||||
assert!(task.started_at.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_complete() {
|
||||
let mut task = Task::default();
|
||||
task.status = TaskStatus::Running;
|
||||
let result = TaskResult::Success("output".to_string());
|
||||
assert!(task.complete(result.clone()).is_ok());
|
||||
assert_eq!(task.status, TaskStatus::Completed);
|
||||
assert!(task.completed_at.is_some());
|
||||
assert_eq!(task.result, Some(result));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_duration_ms() {
|
||||
let mut task = Task::default();
|
||||
task.start().expect("task should start");
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
task.complete(TaskResult::Success("done".to_string()))
|
||||
.expect("task should complete");
|
||||
|
||||
let duration = task.duration_ms();
|
||||
assert!(duration.is_some());
|
||||
assert!(duration.unwrap() >= 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_retry() {
|
||||
let mut task = Task::default();
|
||||
task.status = TaskStatus::Failed;
|
||||
task.result = Some(TaskResult::Error("failed".to_string()));
|
||||
assert!(task.retry().is_ok());
|
||||
assert_eq!(task.status, TaskStatus::Pending);
|
||||
assert!(task.result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_default_values() {
|
||||
let task = Task::default();
|
||||
assert_eq!(task.status, TaskStatus::Pending);
|
||||
assert!(task.result.is_none());
|
||||
assert!(task.logs.is_empty());
|
||||
assert!(task.started_at.is_none());
|
||||
assert!(task.completed_at.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_serialization_roundtrip() {
|
||||
let original = Task::new(Uuid::new_v4(), "Test task".to_string());
|
||||
let serialized = serde_json::to_string(&original).unwrap();
|
||||
let deserialized: Task = serde_json::from_str(&serialized).unwrap();
|
||||
|
||||
assert_eq!(original.id, deserialized.id);
|
||||
assert_eq!(original.todo_id, deserialized.todo_id);
|
||||
assert_eq!(original.description, deserialized.description);
|
||||
assert_eq!(original.status, deserialized.status);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_fail() {
|
||||
let mut task = Task::default();
|
||||
task.status = TaskStatus::Running;
|
||||
assert!(task.fail("error message".to_string()).is_ok());
|
||||
assert_eq!(task.status, TaskStatus::Failed);
|
||||
assert!(task.result.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_invalid_transition() {
|
||||
let mut task = Task::default();
|
||||
// Cannot complete a pending task directly
|
||||
let result = TaskResult::Success("done".to_string());
|
||||
assert!(task.complete(result).is_err());
|
||||
}
|
||||
}
|
||||
320
src/todo.rs
Normal file
320
src/todo.rs
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
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<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 { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_todo_default_values() {
|
||||
let todo = ToDo::default();
|
||||
assert_eq!(todo.status, ToDoStatus::Pending);
|
||||
assert_eq!(todo.priority, DEFAULT_PRIORITY);
|
||||
assert!(todo.depends_on.is_empty());
|
||||
assert!(!todo.id.is_nil());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_todo_serialization_roundtrip() {
|
||||
let original = ToDo::new(
|
||||
"Test Title".to_string(),
|
||||
"Test Description".to_string(),
|
||||
Uuid::new_v4(),
|
||||
);
|
||||
|
||||
let serialized = serde_json::to_string(&original).unwrap();
|
||||
let deserialized: ToDo = serde_json::from_str(&serialized).unwrap();
|
||||
|
||||
assert_eq!(original.id, deserialized.id);
|
||||
assert_eq!(original.title, deserialized.title);
|
||||
assert_eq!(original.description, deserialized.description);
|
||||
assert_eq!(original.status, deserialized.status);
|
||||
assert_eq!(original.priority, deserialized.priority);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_todo_update_title() {
|
||||
let mut todo = ToDo::default();
|
||||
let result = todo.update(Some("New Title".to_string()), None, None);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(todo.title, "New Title");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_todo_update_empty_title() {
|
||||
let mut todo = ToDo::default();
|
||||
let result = todo.update(Some("".to_string()), None, None);
|
||||
assert!(matches!(result, Err(ValidationError::EmptyTitle)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_todo_update_priority() {
|
||||
let mut todo = ToDo::default();
|
||||
let result = todo.update(None, None, Some(50));
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(todo.priority, 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_todo_update_invalid_priority() {
|
||||
let mut todo = ToDo::default();
|
||||
let result = todo.update(None, None, Some(0));
|
||||
assert!(matches!(result, Err(ValidationError::InvalidPriority)));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue