1use chrono::{DateTime, Duration, Utc};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use uuid::Uuid;
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub struct AgentId(pub String);
9
10impl fmt::Display for AgentId {
11 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12 write!(f, "{}", self.0)
13 }
14}
15
16impl From<&str> for AgentId {
17 fn from(s: &str) -> Self {
18 AgentId(s.to_string())
19 }
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub enum EventKind {
25 Meeting,
26 Checkpoint,
27 Review,
28 Deploy,
29 Custom(String),
30}
31
32impl fmt::Display for EventKind {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 EventKind::Meeting => write!(f, "meeting"),
36 EventKind::Checkpoint => write!(f, "checkpoint"),
37 EventKind::Review => write!(f, "review"),
38 EventKind::Deploy => write!(f, "deploy"),
39 EventKind::Custom(s) => write!(f, "custom:{}", s),
40 }
41 }
42}
43
44impl std::str::FromStr for EventKind {
45 type Err = String;
46 fn from_str(s: &str) -> Result<Self, Self::Err> {
47 match s.to_lowercase().as_str() {
48 "meeting" => Ok(EventKind::Meeting),
49 "checkpoint" => Ok(EventKind::Checkpoint),
50 "review" => Ok(EventKind::Review),
51 "deploy" => Ok(EventKind::Deploy),
52 other => Ok(EventKind::Custom(other.to_string())),
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub enum ResponseStatus {
60 Pending,
61 Confirmed,
62 Deferred(Duration),
63 Missed,
64}
65
66impl fmt::Display for ResponseStatus {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 ResponseStatus::Pending => write!(f, "pending"),
70 ResponseStatus::Confirmed => write!(f, "confirmed"),
71 ResponseStatus::Deferred(d) => write!(f, "deferred:{}s", d.num_seconds()),
72 ResponseStatus::Missed => write!(f, "missed"),
73 }
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct TMinusEvent {
80 pub id: Uuid,
81 pub kind: EventKind,
82 pub scheduled_at: DateTime<Utc>,
83 pub t_minus: Duration,
84 pub organizer: AgentId,
85 pub attendees: Vec<(AgentId, ResponseStatus)>,
86 pub quorum: usize,
87 pub payload: serde_json::Value,
88}
89
90impl TMinusEvent {
91 pub fn fire_time(&self) -> DateTime<Utc> {
93 self.scheduled_at - self.t_minus
94 }
95
96 pub fn has_quorum(&self) -> bool {
98 self.confirmed_count() >= self.quorum
99 }
100
101 pub fn confirmed_count(&self) -> usize {
103 self.attendees
104 .iter()
105 .filter(|(_, s)| matches!(s, ResponseStatus::Confirmed))
106 .count()
107 }
108
109 pub fn pending_count(&self) -> usize {
111 self.attendees
112 .iter()
113 .filter(|(_, s)| matches!(s, ResponseStatus::Pending))
114 .count()
115 }
116
117 pub fn is_fired(&self, now: DateTime<Utc>) -> bool {
119 now >= self.fire_time() && self.has_quorum()
120 }
121
122 pub fn is_missed(&self, now: DateTime<Utc>) -> bool {
124 now >= self.fire_time() && !self.has_quorum()
125 }
126
127 pub fn time_remaining(&self, now: DateTime<Utc>) -> Duration {
129 self.fire_time() - now
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct Campaign {
136 pub id: Uuid,
137 pub name: String,
138 pub events: Vec<Uuid>,
139 pub dependencies: Vec<(Uuid, Uuid)>,
140}
141
142impl Campaign {
143 pub fn new(name: String) -> Self {
144 Campaign {
145 id: Uuid::new_v4(),
146 name,
147 events: Vec::new(),
148 dependencies: Vec::new(),
149 }
150 }
151
152 pub fn execution_order(&self) -> Result<Vec<Uuid>, Vec<Uuid>> {
155 let mut in_degree: std::collections::HashMap<Uuid, usize> = std::collections::HashMap::new();
156 let mut adj: std::collections::HashMap<Uuid, Vec<Uuid>> = std::collections::HashMap::new();
157
158 for id in &self.events {
159 in_degree.insert(*id, 0);
160 adj.insert(*id, Vec::new());
161 }
162
163 for (from, to) in &self.dependencies {
164 adj.get_mut(from).unwrap().push(*to);
165 *in_degree.entry(*to).or_insert(0) += 1;
166 }
167
168 let mut queue: std::collections::VecDeque<Uuid> = std::collections::VecDeque::new();
169 for (id, °) in &in_degree {
170 if deg == 0 {
171 queue.push_back(*id);
172 }
173 }
174
175 let mut sorted = Vec::new();
176 while let Some(id) = queue.pop_front() {
177 sorted.push(id);
178 if let Some(neighbors) = adj.get(&id) {
179 for &next in neighbors {
180 let deg = in_degree.get_mut(&next).unwrap();
181 *deg -= 1;
182 if *deg == 0 {
183 queue.push_back(next);
184 }
185 }
186 }
187 }
188
189 if sorted.len() == self.events.len() {
190 Ok(sorted)
191 } else {
192 let cycle: Vec<Uuid> = self.events.iter().filter(|e| !sorted.contains(e)).copied().collect();
194 Err(cycle)
195 }
196 }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct TickResult {
202 pub fired: Vec<Uuid>,
203 pub missed: Vec<Uuid>,
204}
205
206#[derive(Debug, thiserror::Error)]
208pub enum TMinusError {
209 #[error("event not found: {0}")]
210 EventNotFound(Uuid),
211 #[error("campaign not found: {0}")]
212 CampaignNotFound(Uuid),
213 #[error("agent {0} not an attendee of event {1}")]
214 NotAttendee(AgentId, Uuid),
215 #[error("dependency cycle detected: {0:?}")]
216 DependencyCycle(Vec<Uuid>),
217 #[error("database error: {0}")]
218 Database(#[from] rusqlite::Error),
219 #[error("serde error: {0}")]
220 Serde(#[from] serde_json::Error),
221 #[error("invalid input: {0}")]
222 InvalidInput(String),
223}