ralph/queue/operations/edit/
apply.rs1use super::key::TaskEditKey;
18use super::parsing::{
19 cycle_status, normalize_rfc3339_input, parse_list, parse_status, parse_task_agent_override,
20};
21use super::validate_input::ensure_now;
22use crate::contracts::{QueueFile, TaskPriority};
23use crate::queue;
24use crate::queue::operations::validate::{ensure_task_id, parse_custom_fields_with_context};
25use anyhow::{Context, Result, anyhow, bail};
26
27#[allow(clippy::too_many_arguments)]
28pub fn apply_task_edit(
29 queue: &mut QueueFile,
30 done: Option<&QueueFile>,
31 task_id: &str,
32 key: TaskEditKey,
33 input: &str,
34 now_rfc3339: &str,
35 id_prefix: &str,
36 id_width: usize,
37 max_dependency_depth: u8,
38) -> Result<()> {
39 let operation = "edit";
40 let needle = ensure_task_id(task_id, operation)?;
41
42 let index = queue
43 .tasks
44 .iter()
45 .position(|t| t.id.trim() == needle)
46 .ok_or_else(|| {
47 anyhow!(
48 "{}",
49 crate::error_messages::task_not_found_for_edit(operation, needle)
50 )
51 })?;
52
53 let previous = queue.tasks.get(index).cloned().ok_or_else(|| {
54 anyhow!(
55 "{}",
56 crate::error_messages::task_not_found_for_edit(operation, needle)
57 )
58 })?;
59
60 let task = queue.tasks.get_mut(index).ok_or_else(|| {
61 anyhow!(
62 "{}",
63 crate::error_messages::task_not_found_for_edit(operation, needle)
64 )
65 })?;
66
67 let trimmed = input.trim();
68
69 match key {
70 TaskEditKey::Title => {
71 if trimmed.is_empty() {
72 bail!(
73 "Queue edit failed (task_id={}, field=title): title cannot be empty. Provide a non-empty title (e.g., 'Fix login bug').",
74 needle
75 );
76 }
77 task.title = trimmed.to_string();
78 }
79 TaskEditKey::Description => {
80 task.description = if trimmed.is_empty() {
81 None
82 } else {
83 Some(trimmed.to_string())
84 };
85 }
86 TaskEditKey::Status => {
87 let next_status = if trimmed.is_empty() {
88 cycle_status(task.status)
89 } else {
90 parse_status(trimmed).with_context(|| {
91 format!("Queue edit failed (task_id={}, field=status)", needle)
92 })?
93 };
94 let now = ensure_now(now_rfc3339)?;
95 queue::apply_status_policy(task, next_status, &now, None)?;
96 }
97 TaskEditKey::Priority => {
98 task.priority = if trimmed.is_empty() {
99 task.priority.cycle()
100 } else {
101 trimmed.parse::<TaskPriority>().with_context(|| {
102 format!("Queue edit failed (task_id={}, field=priority)", needle)
103 })?
104 };
105 }
106 TaskEditKey::Tags => {
107 task.tags = parse_list(trimmed);
108 }
109 TaskEditKey::Scope => {
110 task.scope = parse_list(trimmed);
111 }
112 TaskEditKey::Evidence => {
113 task.evidence = parse_list(trimmed);
114 }
115 TaskEditKey::Plan => {
116 task.plan = parse_list(trimmed);
117 }
118 TaskEditKey::Notes => {
119 task.notes = parse_list(trimmed);
120 }
121 TaskEditKey::Request => {
122 task.request = if trimmed.is_empty() {
123 None
124 } else {
125 Some(trimmed.to_string())
126 };
127 }
128 TaskEditKey::DependsOn => {
129 task.depends_on = parse_list(trimmed);
130 }
131 TaskEditKey::Blocks => {
132 task.blocks = parse_list(trimmed);
133 }
134 TaskEditKey::RelatesTo => {
135 task.relates_to = parse_list(trimmed);
136 }
137 TaskEditKey::Duplicates => {
138 task.duplicates = if trimmed.is_empty() {
139 None
140 } else {
141 Some(trimmed.to_string())
142 };
143 }
144 TaskEditKey::CustomFields => {
145 task.custom_fields = parse_custom_fields_with_context(needle, trimmed, operation)?;
146 }
147 TaskEditKey::Agent => {
148 task.agent = parse_task_agent_override(trimmed)
149 .with_context(|| format!("Queue edit failed (task_id={}, field=agent)", needle))?;
150 }
151 TaskEditKey::CreatedAt => {
152 let normalized = normalize_rfc3339_input("created_at", trimmed).with_context(|| {
153 format!("Queue edit failed (task_id={}, field=created_at)", needle)
154 })?;
155 task.created_at = normalized;
156 }
157 TaskEditKey::UpdatedAt => {
158 let normalized = normalize_rfc3339_input("updated_at", trimmed).with_context(|| {
159 format!("Queue edit failed (task_id={}, field=updated_at)", needle)
160 })?;
161 task.updated_at = normalized;
162 }
163 TaskEditKey::CompletedAt => {
164 let normalized =
165 normalize_rfc3339_input("completed_at", trimmed).with_context(|| {
166 format!("Queue edit failed (task_id={}, field=completed_at)", needle)
167 })?;
168 task.completed_at = normalized;
169 }
170 TaskEditKey::StartedAt => {
171 let normalized = normalize_rfc3339_input("started_at", trimmed).with_context(|| {
172 format!("Queue edit failed (task_id={}, field=started_at)", needle)
173 })?;
174 task.started_at = normalized;
175 }
176 TaskEditKey::ScheduledStart => {
177 let normalized =
178 normalize_rfc3339_input("scheduled_start", trimmed).with_context(|| {
179 format!(
180 "Queue edit failed (task_id={}, field=scheduled_start)",
181 needle
182 )
183 })?;
184 task.scheduled_start = normalized;
185 }
186 TaskEditKey::EstimatedMinutes => {
187 let minutes = if trimmed.is_empty() {
188 None
189 } else {
190 Some(trimmed.parse::<u32>().with_context(|| {
191 format!(
192 "Queue edit failed (task_id={}, field=estimated_minutes): must be a non-negative integer",
193 needle
194 )
195 })?)
196 };
197 task.estimated_minutes = minutes;
198 }
199 TaskEditKey::ActualMinutes => {
200 let minutes = if trimmed.is_empty() {
201 None
202 } else {
203 Some(trimmed.parse::<u32>().with_context(|| {
204 format!(
205 "Queue edit failed (task_id={}, field=actual_minutes): must be a non-negative integer",
206 needle
207 )
208 })?)
209 };
210 task.actual_minutes = minutes;
211 }
212 }
213
214 if !matches!(key, TaskEditKey::UpdatedAt) {
215 let now = ensure_now(now_rfc3339)?;
216 task.updated_at = Some(now.to_string());
217 }
218
219 match queue::validate_queue_set(queue, done, id_prefix, id_width, max_dependency_depth) {
220 Ok(warnings) => {
221 queue::log_warnings(&warnings);
222 }
223 Err(err) => {
224 queue.tasks[index] = previous;
225 return Err(err);
226 }
227 }
228
229 Ok(())
230}