ralph/config/validation/
queue.rs1use crate::contracts::{QueueAgingThresholds, QueueConfig};
16use anyhow::{Result, bail};
17use std::path::Path;
18
19pub const ERR_EMPTY_QUEUE_ID_PREFIX: &str = "Empty queue.id_prefix: prefix is required if specified. Set a non-empty prefix (e.g., 'RQ') in .ralph/config.jsonc or via --id-prefix.";
20pub const ERR_INVALID_QUEUE_ID_WIDTH: &str = "Invalid queue.id_width: width must be greater than 0. Set a valid width (e.g., 4) in .ralph/config.jsonc or via --id-width.";
21pub const ERR_EMPTY_QUEUE_FILE: &str = "Empty queue.file: path is required if specified. Specify a valid path (e.g., '.ralph/queue.jsonc') in .ralph/config.jsonc or via --queue-file.";
22pub const ERR_EMPTY_QUEUE_DONE_FILE: &str = "Empty queue.done_file: path is required if specified. Specify a valid path (e.g., '.ralph/done.jsonc') in .ralph/config.jsonc or via --done-file.";
23
24pub fn validate_queue_id_prefix_override(id_prefix: Option<&str>) -> Result<()> {
25 if let Some(prefix) = id_prefix
26 && prefix.trim().is_empty()
27 {
28 bail!(ERR_EMPTY_QUEUE_ID_PREFIX);
29 }
30 Ok(())
31}
32
33pub fn validate_queue_id_width_override(id_width: Option<u8>) -> Result<()> {
34 if let Some(width) = id_width
35 && width == 0
36 {
37 bail!(ERR_INVALID_QUEUE_ID_WIDTH);
38 }
39 Ok(())
40}
41
42pub fn validate_queue_file_override(file: Option<&Path>) -> Result<()> {
43 if let Some(path) = file
44 && path.as_os_str().is_empty()
45 {
46 bail!(ERR_EMPTY_QUEUE_FILE);
47 }
48 Ok(())
49}
50
51pub fn validate_queue_done_file_override(done_file: Option<&Path>) -> Result<()> {
52 if let Some(path) = done_file
53 && path.as_os_str().is_empty()
54 {
55 bail!(ERR_EMPTY_QUEUE_DONE_FILE);
56 }
57 Ok(())
58}
59
60pub fn validate_queue_overrides(queue: &QueueConfig) -> Result<()> {
61 validate_queue_id_prefix_override(queue.id_prefix.as_deref())?;
62 validate_queue_id_width_override(queue.id_width)?;
63 validate_queue_file_override(queue.file.as_deref())?;
64 validate_queue_done_file_override(queue.done_file.as_deref())?;
65 validate_queue_thresholds(queue)?;
66 Ok(())
67}
68
69pub fn validate_queue_thresholds(queue: &QueueConfig) -> Result<()> {
70 if let Some(threshold) = queue.size_warning_threshold_kb
71 && !(100..=10000).contains(&threshold)
72 {
73 bail!(
74 "Invalid queue.size_warning_threshold_kb: {}. Value must be between 100 and 10000 (inclusive). Update .ralph/config.jsonc.",
75 threshold
76 );
77 }
78
79 if let Some(threshold) = queue.task_count_warning_threshold
80 && !(50..=5000).contains(&threshold)
81 {
82 bail!(
83 "Invalid queue.task_count_warning_threshold: {}. Value must be between 50 and 5000 (inclusive). Update .ralph/config.jsonc.",
84 threshold
85 );
86 }
87
88 if let Some(depth) = queue.max_dependency_depth
89 && !(1..=100).contains(&depth)
90 {
91 bail!(
92 "Invalid queue.max_dependency_depth: {}. Value must be between 1 and 100 (inclusive). Update .ralph/config.jsonc.",
93 depth
94 );
95 }
96
97 if let Some(days) = queue.auto_archive_terminal_after_days
98 && days > 3650
99 {
100 bail!(
101 "Invalid queue.auto_archive_terminal_after_days: {}. Value must be between 0 and 3650 (inclusive). Update .ralph/config.jsonc.",
102 days
103 );
104 }
105
106 Ok(())
107}
108
109pub fn validate_queue_aging_thresholds(thresholds: &Option<QueueAgingThresholds>) -> Result<()> {
110 let Some(thresholds) = thresholds else {
111 return Ok(());
112 };
113
114 let warning = thresholds.warning_days;
115 let stale = thresholds.stale_days;
116 let rotten = thresholds.rotten_days;
117
118 if let (Some(w), Some(s)) = (warning, stale)
119 && w >= s
120 {
121 bail!(format_aging_threshold_error(Some(w), Some(s), rotten));
122 }
123 if let (Some(s), Some(r)) = (stale, rotten)
124 && s >= r
125 {
126 bail!(format_aging_threshold_error(warning, Some(s), Some(r)));
127 }
128 if let (Some(w), Some(r)) = (warning, rotten)
129 && w >= r
130 {
131 bail!(format_aging_threshold_error(Some(w), stale, Some(r)));
132 }
133
134 Ok(())
135}
136
137fn format_aging_threshold_error(
138 warning: Option<u32>,
139 stale: Option<u32>,
140 rotten: Option<u32>,
141) -> String {
142 format!(
143 "Invalid queue.aging_thresholds ordering: require warning_days < stale_days < rotten_days (got warning_days={}, stale_days={}, rotten_days={}). Update .ralph/config.jsonc.",
144 warning
145 .map(|value| value.to_string())
146 .unwrap_or_else(|| "unset".to_string()),
147 stale
148 .map(|value| value.to_string())
149 .unwrap_or_else(|| "unset".to_string()),
150 rotten
151 .map(|value| value.to_string())
152 .unwrap_or_else(|| "unset".to_string()),
153 )
154}