Commons & STP
This commit is contained in:
parent
47084819e0
commit
958cf8829a
9 changed files with 1188 additions and 0 deletions
17
types/Cargo.toml
Normal file
17
types/Cargo.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[package]
|
||||||
|
name = "shoal-types"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
uuid = { workspace = true }
|
||||||
|
chrono = { workspace = true }
|
||||||
|
thiserror.workspace = true
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
stp-core = { path = "../../stp/core" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio.workspace = true
|
||||||
138
types/src/coral.rs
Normal file
138
types/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()]));
|
||||||
|
}
|
||||||
|
}
|
||||||
189
types/src/enums.rs
Normal file
189
types/src/enums.rs
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
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)]
|
||||||
|
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)]
|
||||||
|
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)]
|
||||||
|
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)]
|
||||||
|
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)]
|
||||||
|
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)]
|
||||||
|
pub enum TaskResultType {
|
||||||
|
Success,
|
||||||
|
Error,
|
||||||
|
Split,
|
||||||
|
}
|
||||||
45
types/src/errors.rs
Normal file
45
types/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
types/src/krill.rs
Normal file
133
types/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
types/src/lib.rs
Normal file
19
types/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
types/src/project.rs
Normal file
195
types/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)]
|
||||||
|
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)]
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
190
types/src/task.rs
Normal file
190
types/src/task.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::enums::{TaskResultType, TaskStatus};
|
||||||
|
use crate::todo::ToDo;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
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)]
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
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