1use crate::Topic;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
10pub struct HatId(String);
11
12impl HatId {
13 pub fn new(id: impl Into<String>) -> Self {
15 Self(id.into())
16 }
17
18 pub fn as_str(&self) -> &str {
20 &self.0
21 }
22}
23
24impl From<&str> for HatId {
25 fn from(s: &str) -> Self {
26 Self::new(s)
27 }
28}
29
30impl From<String> for HatId {
31 fn from(s: String) -> Self {
32 Self::new(s)
33 }
34}
35
36impl std::fmt::Display for HatId {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 write!(f, "{}", self.0)
39 }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Hat {
45 pub id: HatId,
47
48 pub name: String,
50
51 pub description: String,
54
55 pub subscriptions: Vec<Topic>,
57
58 pub publishes: Vec<Topic>,
60
61 pub instructions: String,
63}
64
65impl Hat {
66 pub fn new(id: impl Into<HatId>, name: impl Into<String>) -> Self {
68 Self {
69 id: id.into(),
70 name: name.into(),
71 description: String::new(),
72 subscriptions: Vec::new(),
73 publishes: Vec::new(),
74 instructions: String::new(),
75 }
76 }
77
78 #[must_use]
80 pub fn with_description(mut self, description: impl Into<String>) -> Self {
81 self.description = description.into();
82 self
83 }
84
85 #[deprecated(note = "Use default_planner() and default_builder() instead")]
87 pub fn default_single() -> Self {
88 Self {
89 id: HatId::new("default"),
90 name: "Default".to_string(),
91 description: "Default single-hat mode handler".to_string(),
92 subscriptions: vec![Topic::new("*")],
93 publishes: vec![Topic::new("task.done")],
94 instructions: String::new(),
95 }
96 }
97
98 pub fn default_planner() -> Self {
103 Self {
104 id: HatId::new("planner"),
105 name: "Planner".to_string(),
106 description: "Plans and prioritizes tasks, delegates to Builder".to_string(),
107 subscriptions: vec![
108 Topic::new("task.start"),
109 Topic::new("task.resume"),
110 Topic::new("build.done"),
111 Topic::new("build.blocked"),
112 ],
113 publishes: vec![Topic::new("build.task")],
114 instructions: String::new(),
115 }
116 }
117
118 pub fn default_builder() -> Self {
123 Self {
124 id: HatId::new("builder"),
125 name: "Builder".to_string(),
126 description: "Implements code changes, runs backpressure".to_string(),
127 subscriptions: vec![Topic::new("build.task")],
128 publishes: vec![Topic::new("build.done"), Topic::new("build.blocked")],
129 instructions: String::new(),
130 }
131 }
132
133 #[must_use]
135 pub fn subscribe(mut self, topic: impl Into<Topic>) -> Self {
136 self.subscriptions.push(topic.into());
137 self
138 }
139
140 #[must_use]
142 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
143 self.instructions = instructions.into();
144 self
145 }
146
147 #[must_use]
149 pub fn with_publishes(mut self, publishes: Vec<Topic>) -> Self {
150 self.publishes = publishes;
151 self
152 }
153
154 pub fn is_subscribed(&self, topic: &Topic) -> bool {
156 self.is_subscribed_str(topic.as_str())
157 }
158
159 pub fn is_subscribed_str(&self, topic: &str) -> bool {
163 self.subscriptions.iter().any(|sub| sub.matches_str(topic))
164 }
165
166 pub fn has_specific_subscription(&self, topic: &Topic) -> bool {
172 self.subscriptions
173 .iter()
174 .any(|sub| !sub.is_global_wildcard() && sub.matches(topic))
175 }
176
177 pub fn is_fallback_only(&self) -> bool {
181 !self.subscriptions.is_empty() && self.subscriptions.iter().all(Topic::is_global_wildcard)
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_subscription_matching() {
191 let hat = Hat::new("impl", "Implementer")
192 .subscribe("impl.*")
193 .subscribe("task.start");
194
195 assert!(hat.is_subscribed(&Topic::new("impl.done")));
196 assert!(hat.is_subscribed(&Topic::new("task.start")));
197 assert!(!hat.is_subscribed(&Topic::new("review.done")));
198 }
199
200 #[test]
201 #[allow(deprecated)]
202 fn test_default_single_hat() {
203 let hat = Hat::default_single();
204 assert!(hat.is_subscribed(&Topic::new("anything")));
205 assert!(hat.is_subscribed(&Topic::new("impl.done")));
206 }
207
208 #[test]
209 fn test_default_planner_hat() {
210 let hat = Hat::default_planner();
211 assert_eq!(hat.id.as_str(), "planner");
212 assert!(hat.is_subscribed(&Topic::new("task.start")));
213 assert!(hat.is_subscribed(&Topic::new("task.resume"))); assert!(hat.is_subscribed(&Topic::new("build.done")));
215 assert!(hat.is_subscribed(&Topic::new("build.blocked")));
216 assert!(!hat.is_subscribed(&Topic::new("build.task")));
217 }
218
219 #[test]
220 fn test_default_builder_hat() {
221 let hat = Hat::default_builder();
222 assert_eq!(hat.id.as_str(), "builder");
223 assert!(hat.is_subscribed(&Topic::new("build.task")));
224 assert!(!hat.is_subscribed(&Topic::new("task.start")));
225 assert!(!hat.is_subscribed(&Topic::new("build.done")));
226 }
227}