1use super::safety::SafetyLevel;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum TaskConstraintsError {
8 InvalidMaxAttempts {
10 max_attempts: u32,
12 },
13 EmptyConcurrencyKey,
15 ZeroTimeout,
17 EmptyCapabilities,
19 EmptyCapabilityEntry,
21}
22
23impl std::fmt::Display for TaskConstraintsError {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 match self {
26 TaskConstraintsError::InvalidMaxAttempts { max_attempts } => {
27 write!(f, "invalid max_attempts value: {max_attempts} (must be >= 1)")
28 }
29 TaskConstraintsError::EmptyConcurrencyKey => {
30 write!(f, "concurrency key must not be an empty string")
31 }
32 TaskConstraintsError::ZeroTimeout => {
33 write!(f, "timeout_secs must not be zero")
34 }
35 TaskConstraintsError::EmptyCapabilities => {
36 write!(f, "required capabilities list must not be empty")
37 }
38 TaskConstraintsError::EmptyCapabilityEntry => {
39 write!(f, "required_capabilities contains an empty string")
40 }
41 }
42 }
43}
44
45impl std::error::Error for TaskConstraintsError {}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
50pub enum ConcurrencyKeyHoldPolicy {
51 #[default]
53 HoldDuringRetry,
54 ReleaseOnRetry,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize))]
61pub struct TaskConstraints {
62 max_attempts: u32,
65 timeout_secs: Option<u64>,
67 concurrency_key: Option<String>,
69 #[cfg_attr(feature = "serde", serde(default))]
71 concurrency_key_hold_policy: ConcurrencyKeyHoldPolicy,
72 #[cfg_attr(feature = "serde", serde(default))]
74 safety_level: SafetyLevel,
75 #[cfg_attr(feature = "serde", serde(default))]
84 required_capabilities: Option<Vec<String>>,
85}
86
87impl TaskConstraints {
88 pub fn new(
90 max_attempts: u32,
91 timeout_secs: Option<u64>,
92 concurrency_key: Option<String>,
93 ) -> Result<Self, TaskConstraintsError> {
94 let constraints = Self {
95 max_attempts,
96 timeout_secs,
97 concurrency_key,
98 concurrency_key_hold_policy: ConcurrencyKeyHoldPolicy::default(),
99 safety_level: SafetyLevel::default(),
100 required_capabilities: None,
101 };
102 constraints.validate()?;
103 Ok(constraints)
104 }
105
106 pub fn validate(&self) -> Result<(), TaskConstraintsError> {
108 if self.max_attempts == 0 {
109 return Err(TaskConstraintsError::InvalidMaxAttempts { max_attempts: 0 });
110 }
111 if self.timeout_secs == Some(0) {
112 return Err(TaskConstraintsError::ZeroTimeout);
113 }
114 if let Some(ref key) = self.concurrency_key {
115 if key.is_empty() {
116 return Err(TaskConstraintsError::EmptyConcurrencyKey);
117 }
118 }
119 if let Some(ref caps) = self.required_capabilities {
120 if caps.is_empty() {
121 return Err(TaskConstraintsError::EmptyCapabilities);
122 }
123 if caps.iter().any(String::is_empty) {
124 return Err(TaskConstraintsError::EmptyCapabilityEntry);
125 }
126 }
127 Ok(())
128 }
129
130 pub fn max_attempts(&self) -> u32 {
132 self.max_attempts
133 }
134
135 pub fn timeout_secs(&self) -> Option<u64> {
137 self.timeout_secs
138 }
139
140 pub fn concurrency_key(&self) -> Option<&str> {
142 self.concurrency_key.as_deref()
143 }
144
145 pub fn set_max_attempts(&mut self, max_attempts: u32) -> Result<(), TaskConstraintsError> {
147 if max_attempts == 0 {
148 return Err(TaskConstraintsError::InvalidMaxAttempts { max_attempts });
149 }
150
151 self.max_attempts = max_attempts;
152 Ok(())
153 }
154
155 pub fn set_timeout_secs(
161 &mut self,
162 timeout_secs: Option<u64>,
163 ) -> Result<(), TaskConstraintsError> {
164 if timeout_secs == Some(0) {
165 return Err(TaskConstraintsError::ZeroTimeout);
166 }
167 self.timeout_secs = timeout_secs;
168 Ok(())
169 }
170
171 pub fn set_concurrency_key(
177 &mut self,
178 concurrency_key: Option<String>,
179 ) -> Result<(), TaskConstraintsError> {
180 if let Some(ref key) = concurrency_key {
181 if key.is_empty() {
182 return Err(TaskConstraintsError::EmptyConcurrencyKey);
183 }
184 }
185 self.concurrency_key = concurrency_key;
186 Ok(())
187 }
188
189 pub fn concurrency_key_hold_policy(&self) -> ConcurrencyKeyHoldPolicy {
191 self.concurrency_key_hold_policy
192 }
193
194 pub fn set_concurrency_key_hold_policy(&mut self, policy: ConcurrencyKeyHoldPolicy) {
196 self.concurrency_key_hold_policy = policy;
197 }
198
199 pub fn safety_level(&self) -> SafetyLevel {
201 self.safety_level
202 }
203
204 pub fn set_safety_level(&mut self, safety_level: SafetyLevel) {
206 self.safety_level = safety_level;
207 }
208
209 pub fn required_capabilities(&self) -> Option<&[String]> {
215 self.required_capabilities.as_deref()
216 }
217
218 pub fn with_capabilities(mut self, caps: Vec<String>) -> Result<Self, TaskConstraintsError> {
222 if caps.is_empty() {
223 return Err(TaskConstraintsError::EmptyCapabilities);
224 }
225 if caps.iter().any(String::is_empty) {
226 return Err(TaskConstraintsError::EmptyCapabilityEntry);
227 }
228 self.required_capabilities = Some(caps);
229 Ok(self)
230 }
231}
232
233#[cfg(feature = "serde")]
234impl<'de> serde::Deserialize<'de> for TaskConstraints {
235 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
236 where
237 D: serde::Deserializer<'de>,
238 {
239 #[derive(serde::Deserialize)]
240 struct TaskConstraintsWire {
241 max_attempts: u32,
242 timeout_secs: Option<u64>,
243 concurrency_key: Option<String>,
244 #[serde(default)]
245 concurrency_key_hold_policy: ConcurrencyKeyHoldPolicy,
246 #[serde(default)]
247 safety_level: SafetyLevel,
248 #[serde(default)]
249 required_capabilities: Option<Vec<String>>,
250 }
251
252 let wire = TaskConstraintsWire::deserialize(deserializer)?;
253 let constraints = TaskConstraints {
254 max_attempts: wire.max_attempts,
255 timeout_secs: wire.timeout_secs,
256 concurrency_key: wire.concurrency_key,
257 concurrency_key_hold_policy: wire.concurrency_key_hold_policy,
258 safety_level: wire.safety_level,
259 required_capabilities: wire.required_capabilities,
260 };
261 constraints.validate().map_err(serde::de::Error::custom)?;
262 Ok(constraints)
263 }
264}
265
266#[cfg(feature = "testing")]
267impl TaskConstraints {
268 pub fn new_for_testing(
273 max_attempts: u32,
274 timeout_secs: Option<u64>,
275 concurrency_key: Option<String>,
276 ) -> Self {
277 Self {
278 max_attempts,
279 timeout_secs,
280 concurrency_key,
281 concurrency_key_hold_policy: ConcurrencyKeyHoldPolicy::default(),
282 safety_level: SafetyLevel::default(),
283 required_capabilities: None,
284 }
285 }
286}
287
288impl Default for TaskConstraints {
289 fn default() -> Self {
290 Self::new(1, None, None).expect("default TaskConstraints must be valid")
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::{TaskConstraints, TaskConstraintsError};
297
298 #[test]
299 fn rejects_zero_max_attempts_at_construction() {
300 let result = TaskConstraints::new(0, Some(10), None);
301 assert_eq!(result, Err(TaskConstraintsError::InvalidMaxAttempts { max_attempts: 0 }));
302 }
303
304 #[test]
305 fn set_max_attempts_rejects_zero_without_mutation() {
306 let mut constraints = TaskConstraints::default();
307 let original = constraints.clone();
308
309 let result = constraints.set_max_attempts(0);
310 assert_eq!(result, Err(TaskConstraintsError::InvalidMaxAttempts { max_attempts: 0 }));
311 assert_eq!(constraints, original);
312 }
313
314 #[test]
315 fn rejects_empty_concurrency_key_at_construction() {
316 let result = TaskConstraints::new(1, None, Some(String::new()));
317 assert_eq!(result, Err(TaskConstraintsError::EmptyConcurrencyKey));
318 }
319
320 #[test]
321 fn accepts_none_concurrency_key() {
322 let result = TaskConstraints::new(1, None, None);
323 assert!(result.is_ok());
324 }
325
326 #[test]
327 fn accepts_valid_concurrency_key() {
328 let result = TaskConstraints::new(1, None, Some("valid-key".to_string()));
329 assert!(result.is_ok());
330 assert_eq!(result.unwrap().concurrency_key(), Some("valid-key"));
331 }
332
333 #[test]
334 fn set_concurrency_key_rejects_empty() {
335 let mut constraints = TaskConstraints::default();
336 let result = constraints.set_concurrency_key(Some(String::new()));
337 assert_eq!(result, Err(TaskConstraintsError::EmptyConcurrencyKey));
338 assert_eq!(constraints.concurrency_key(), None); }
340
341 #[test]
342 fn set_concurrency_key_accepts_valid() {
343 let mut constraints = TaskConstraints::default();
344 constraints.set_concurrency_key(Some("key".to_string())).unwrap();
345 assert_eq!(constraints.concurrency_key(), Some("key"));
346 }
347
348 #[test]
349 fn set_concurrency_key_accepts_none() {
350 let mut constraints = TaskConstraints::new(1, None, Some("key".to_string())).unwrap();
351 constraints.set_concurrency_key(None).unwrap();
352 assert_eq!(constraints.concurrency_key(), None);
353 }
354
355 #[test]
356 fn rejects_zero_timeout_at_construction() {
357 let result = TaskConstraints::new(1, Some(0), None);
358 assert_eq!(result, Err(TaskConstraintsError::ZeroTimeout));
359 }
360
361 #[test]
362 fn set_timeout_secs_rejects_zero() {
363 let mut constraints = TaskConstraints::default();
364 let result = constraints.set_timeout_secs(Some(0));
365 assert_eq!(result, Err(TaskConstraintsError::ZeroTimeout));
366 assert_eq!(constraints.timeout_secs(), None); }
368
369 #[test]
370 fn set_timeout_secs_accepts_none() {
371 let mut constraints = TaskConstraints::new(1, Some(30), None).unwrap();
372 constraints.set_timeout_secs(None).unwrap();
373 assert_eq!(constraints.timeout_secs(), None);
374 }
375
376 #[test]
377 fn set_timeout_secs_accepts_positive_value() {
378 let mut constraints = TaskConstraints::default();
379 constraints.set_timeout_secs(Some(60)).unwrap();
380 assert_eq!(constraints.timeout_secs(), Some(60));
381 }
382
383 #[test]
384 fn accepts_valid_timeout_at_construction() {
385 let result = TaskConstraints::new(1, Some(30), None);
386 assert!(result.is_ok());
387 assert_eq!(result.unwrap().timeout_secs(), Some(30));
388 }
389
390 #[test]
391 fn with_capabilities_rejects_empty_vec() {
392 let constraints = TaskConstraints::default();
393 let result = constraints.with_capabilities(vec![]);
394 assert_eq!(result, Err(TaskConstraintsError::EmptyCapabilities));
395 }
396
397 #[test]
398 fn with_capabilities_accepts_non_empty_vec() {
399 let constraints = TaskConstraints::default();
400 let result = constraints.with_capabilities(vec!["gpu".to_string()]);
401 assert!(result.is_ok());
402 assert_eq!(result.unwrap().required_capabilities(), Some(&["gpu".to_string()][..]));
403 }
404
405 #[test]
406 fn validate_passes_for_default_and_valid_capabilities() {
407 let mut constraints = TaskConstraints::default();
410 assert!(constraints.validate().is_ok());
411 constraints = constraints.with_capabilities(vec!["cap1".to_string()]).unwrap();
412 assert!(constraints.validate().is_ok());
413 }
414
415 #[test]
416 fn with_capabilities_rejects_empty_string_entry() {
417 let constraints = TaskConstraints::default();
418 let result = constraints.with_capabilities(vec!["gpu".to_string(), String::new()]);
419 assert_eq!(result, Err(TaskConstraintsError::EmptyCapabilityEntry));
420 }
421}