use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap;
fn is_zero<T: Default + PartialEq>(v: &T) -> bool {
*v == T::default()
}
fn is_default_priority(p: &Priority) -> bool {
*p == PRIORITY_DEFAULT
}
mod metrics_serde {
use super::*;
pub fn serialize<S: Serializer>(metrics: &[i64; 8], s: S) -> Result<S::Ok, S::Error> {
let len = metrics
.iter()
.rposition(|&x| x != 0)
.map(|i| i + 1)
.unwrap_or(0);
s.collect_seq(&metrics[..len])
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[i64; 8], D::Error> {
let v: Vec<i64> = Vec::deserialize(d)?;
let mut arr = [0i64; 8];
for (i, val) in v.into_iter().take(8).enumerate() {
arr[i] = val;
}
Ok(arr)
}
pub fn is_empty(metrics: &[i64; 8]) -> bool {
metrics.iter().all(|&x| x == 0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Worker {
pub id: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
pub max_claims: i32,
pub registered_at: i64,
pub last_heartbeat: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_phase: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workflow: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub overlays: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerInfo {
pub id: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
pub max_claims: i32,
#[serde(skip_serializing_if = "is_zero")]
pub claim_count: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_thought: Option<String>,
pub registered_at: i64,
pub last_heartbeat: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_phase: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workflow: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub overlays: Vec<String>,
}
pub type Priority = i32;
pub const PRIORITY_DEFAULT: Priority = 5;
pub fn parse_priority(s: &str) -> Priority {
s.parse().unwrap_or(PRIORITY_DEFAULT).clamp(0, 10)
}
pub fn clamp_priority(p: Priority) -> Priority {
p.clamp(0, 10)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
pub id: String,
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub phase: Option<String>,
#[serde(skip_serializing_if = "is_default_priority")]
pub priority: Priority,
#[serde(skip_serializing_if = "Option::is_none")]
pub worker_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub claimed_at: Option<i64>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub needed_tags: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub wanted_tags: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub points: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_estimate_ms: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_actual_ms: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub started_at: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_thought: Option<String>,
#[serde(skip_serializing_if = "is_zero")]
pub cost_usd: f64,
#[serde(
with = "metrics_serde",
skip_serializing_if = "metrics_serde::is_empty",
default
)]
pub metrics: [i64; 8],
pub created_at: i64,
pub updated_at: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskTree {
#[serde(flatten)]
pub task: Task,
pub children: Vec<TaskTree>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskTreeInput {
#[serde(rename = "ref")]
pub ref_id: Option<String>,
pub id: Option<String>,
pub title: Option<String>,
pub description: Option<String>,
pub phase: Option<String>,
pub priority: Option<Priority>,
pub points: Option<i32>,
pub time_estimate_ms: Option<i64>,
pub needed_tags: Option<Vec<String>>,
pub wanted_tags: Option<Vec<String>>,
pub tags: Option<Vec<String>>,
#[serde(default)]
pub children: Vec<TaskTreeInput>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dependency {
pub from_task_id: String,
pub to_task_id: String,
pub dep_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileLock {
pub file_path: String,
pub worker_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
pub locked_at: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaimEvent {
pub id: i64,
pub file_path: String,
pub worker_id: String,
pub event: ClaimEventType,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
pub timestamp: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_timestamp: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub claim_id: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskSequenceEvent {
pub id: i64,
pub task_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub worker_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phase: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
pub timestamp: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_timestamp: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskStateEvent {
pub id: i64,
pub task_id: String,
pub worker_id: Option<String>,
pub event: String,
pub reason: Option<String>,
pub timestamp: i64,
pub end_timestamp: Option<i64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ClaimEventType {
Claimed,
Released,
}
impl ClaimEventType {
pub fn as_str(&self) -> &'static str {
match self {
ClaimEventType::Claimed => "claimed",
ClaimEventType::Released => "released",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"claimed" => Some(ClaimEventType::Claimed),
"released" => Some(ClaimEventType::Released),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaimUpdates {
pub new_claims: Vec<ClaimEvent>,
pub dropped_claims: Vec<ClaimEvent>,
pub sequence: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Attachment {
pub task_id: String,
pub attachment_type: String,
pub sequence: i32,
pub name: String,
pub mime_type: String,
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_path: Option<String>,
pub created_at: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttachmentMeta {
pub task_id: String,
pub attachment_type: String,
pub sequence: i32,
pub name: String,
pub mime_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_path: Option<String>,
pub created_at: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Stats {
pub total_tasks: i64,
pub tasks_by_status: HashMap<String, i64>,
#[serde(skip_serializing_if = "is_zero")]
pub total_points: i64,
#[serde(skip_serializing_if = "is_zero")]
pub completed_points: i64,
#[serde(skip_serializing_if = "is_zero")]
pub total_time_estimate_ms: i64,
#[serde(skip_serializing_if = "is_zero")]
pub total_time_actual_ms: i64,
#[serde(skip_serializing_if = "is_zero")]
pub total_cost_usd: f64,
#[serde(
with = "metrics_serde",
skip_serializing_if = "metrics_serde::is_empty",
default
)]
pub total_metrics: [i64; 8],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskSummary {
pub id: String,
pub title: String,
pub status: String,
#[serde(skip_serializing_if = "is_default_priority")]
pub priority: Priority,
#[serde(skip_serializing_if = "Option::is_none")]
pub worker_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub points: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_thought: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanResult {
pub root: Task,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub before: Vec<Task>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub after: Vec<Task>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub above: Vec<Task>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub below: Vec<Task>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisconnectSummary {
pub tasks_released: i32,
pub files_released: i32,
pub final_status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CleanupSummary {
pub workers_evicted: i32,
pub tasks_released: i32,
pub files_released: i32,
pub final_status: String,
pub evicted_worker_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskTagRow {
pub task_id: String,
pub tag: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskNeededTagRow {
pub task_id: String,
pub tag: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskWantedTagRow {
pub task_id: String,
pub tag: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ExportTables {
#[serde(skip_serializing_if = "Option::is_none")]
pub tasks: Option<Vec<Task>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<Vec<Dependency>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<Attachment>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_tags: Option<Vec<TaskTagRow>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_needed_tags: Option<Vec<TaskNeededTagRow>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_wanted_tags: Option<Vec<TaskWantedTagRow>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_sequence: Option<Vec<TaskSequenceEvent>>,
}
#[cfg(test)]
mod tests {
}