use chrono::SecondsFormat;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap;
pub fn ms_to_iso(ms: i64) -> String {
chrono::DateTime::from_timestamp_millis(ms)
.unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).unwrap())
.to_rfc3339_opts(SecondsFormat::Secs, true)
}
pub mod timestamp_serde {
use super::*;
pub fn serialize<S: Serializer>(ms: &i64, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&super::ms_to_iso(*ms))
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<i64, D::Error> {
let v: serde_json::Value = Deserialize::deserialize(d)?;
match v {
serde_json::Value::Number(n) => n
.as_i64()
.ok_or_else(|| serde::de::Error::custom("invalid timestamp number")),
serde_json::Value::String(s) => {
if let Ok(ms) = s.parse::<i64>() {
return Ok(ms);
}
chrono::DateTime::parse_from_rfc3339(&s)
.map(|dt| dt.timestamp_millis())
.map_err(|e| {
serde::de::Error::custom(format!("invalid timestamp string: {}", e))
})
}
_ => Err(serde::de::Error::custom(
"expected number or string for timestamp",
)),
}
}
}
pub mod timestamp_opt_serde {
use super::*;
pub fn serialize<S: Serializer>(ms: &Option<i64>, s: S) -> Result<S::Ok, S::Error> {
match ms {
Some(v) => s.serialize_str(&super::ms_to_iso(*v)),
None => s.serialize_none(),
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<i64>, D::Error> {
let v: Option<serde_json::Value> = Option::deserialize(d)?;
match v {
None | Some(serde_json::Value::Null) => Ok(None),
Some(serde_json::Value::Number(n)) => n
.as_i64()
.map(Some)
.ok_or_else(|| serde::de::Error::custom("invalid timestamp number")),
Some(serde_json::Value::String(s)) => {
if let Ok(ms) = s.parse::<i64>() {
return Ok(Some(ms));
}
chrono::DateTime::parse_from_rfc3339(&s)
.map(|dt| Some(dt.timestamp_millis()))
.map_err(|e| {
serde::de::Error::custom(format!("invalid timestamp string: {}", e))
})
}
_ => Err(serde::de::Error::custom(
"expected number or string for timestamp",
)),
}
}
}
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,
#[serde(with = "timestamp_serde")]
pub registered_at: i64,
#[serde(with = "timestamp_serde")]
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 last_task_id: 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>,
#[serde(with = "timestamp_serde")]
pub registered_at: i64,
#[serde(with = "timestamp_serde")]
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 last_task_id: 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",
default,
with = "timestamp_opt_serde"
)]
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",
default,
with = "timestamp_opt_serde"
)]
pub started_at: Option<i64>,
#[serde(
skip_serializing_if = "Option::is_none",
default,
with = "timestamp_opt_serde"
)]
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],
#[serde(with = "timestamp_serde")]
pub created_at: i64,
#[serde(with = "timestamp_serde")]
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 blocked_by: 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>,
#[serde(with = "timestamp_serde")]
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>,
#[serde(with = "timestamp_serde")]
pub timestamp: i64,
#[serde(
skip_serializing_if = "Option::is_none",
default,
with = "timestamp_opt_serde"
)]
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>,
#[serde(with = "timestamp_serde")]
pub timestamp: i64,
#[serde(
skip_serializing_if = "Option::is_none",
default,
with = "timestamp_opt_serde"
)]
pub end_timestamp: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub concurrency: Option<i32>,
}
#[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>,
#[serde(with = "timestamp_serde")]
pub timestamp: i64,
#[serde(default, with = "timestamp_opt_serde")]
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>,
#[serde(with = "timestamp_serde")]
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>,
#[serde(with = "timestamp_serde")]
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 {
}