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::Status => {
80 let next_status = if trimmed.is_empty() {
81 cycle_status(task.status)
82 } else {
83 parse_status(trimmed).with_context(|| {
84 format!("Queue edit failed (task_id={}, field=status)", needle)
85 })?
86 };
87 let now = ensure_now(now_rfc3339)?;
88 queue::apply_status_policy(task, next_status, &now, None)?;
89 }
90 TaskEditKey::Priority => {
91 task.priority = if trimmed.is_empty() {
92 task.priority.cycle()
93 } else {
94 trimmed.parse::<TaskPriority>().with_context(|| {
95 format!("Queue edit failed (task_id={}, field=priority)", needle)
96 })?
97 };
98 }
99 TaskEditKey::Tags => {
100 task.tags = parse_list(trimmed);
101 }
102 TaskEditKey::Scope => {
103 task.scope = parse_list(trimmed);
104 }
105 TaskEditKey::Evidence => {
106 task.evidence = parse_list(trimmed);
107 }
108 TaskEditKey::Plan => {
109 task.plan = parse_list(trimmed);
110 }
111 TaskEditKey::Notes => {
112 task.notes = parse_list(trimmed);
113 }
114 TaskEditKey::Request => {
115 task.request = if trimmed.is_empty() {
116 None
117 } else {
118 Some(trimmed.to_string())
119 };
120 }
121 TaskEditKey::DependsOn => {
122 task.depends_on = parse_list(trimmed);
123 }
124 TaskEditKey::Blocks => {
125 task.blocks = parse_list(trimmed);
126 }
127 TaskEditKey::RelatesTo => {
128 task.relates_to = parse_list(trimmed);
129 }
130 TaskEditKey::Duplicates => {
131 task.duplicates = if trimmed.is_empty() {
132 None
133 } else {
134 Some(trimmed.to_string())
135 };
136 }
137 TaskEditKey::CustomFields => {
138 task.custom_fields = parse_custom_fields_with_context(needle, trimmed, operation)?;
139 }
140 TaskEditKey::Agent => {
141 task.agent = parse_task_agent_override(trimmed)
142 .with_context(|| format!("Queue edit failed (task_id={}, field=agent)", needle))?;
143 }
144 TaskEditKey::CreatedAt => {
145 let normalized = normalize_rfc3339_input("created_at", trimmed).with_context(|| {
146 format!("Queue edit failed (task_id={}, field=created_at)", needle)
147 })?;
148 task.created_at = normalized;
149 }
150 TaskEditKey::UpdatedAt => {
151 let normalized = normalize_rfc3339_input("updated_at", trimmed).with_context(|| {
152 format!("Queue edit failed (task_id={}, field=updated_at)", needle)
153 })?;
154 task.updated_at = normalized;
155 }
156 TaskEditKey::CompletedAt => {
157 let normalized =
158 normalize_rfc3339_input("completed_at", trimmed).with_context(|| {
159 format!("Queue edit failed (task_id={}, field=completed_at)", needle)
160 })?;
161 task.completed_at = normalized;
162 }
163 TaskEditKey::StartedAt => {
164 let normalized = normalize_rfc3339_input("started_at", trimmed).with_context(|| {
165 format!("Queue edit failed (task_id={}, field=started_at)", needle)
166 })?;
167 task.started_at = normalized;
168 }
169 TaskEditKey::ScheduledStart => {
170 let normalized =
171 normalize_rfc3339_input("scheduled_start", trimmed).with_context(|| {
172 format!(
173 "Queue edit failed (task_id={}, field=scheduled_start)",
174 needle
175 )
176 })?;
177 task.scheduled_start = normalized;
178 }
179 TaskEditKey::EstimatedMinutes => {
180 let minutes = if trimmed.is_empty() {
181 None
182 } else {
183 Some(trimmed.parse::<u32>().with_context(|| {
184 format!(
185 "Queue edit failed (task_id={}, field=estimated_minutes): must be a non-negative integer",
186 needle
187 )
188 })?)
189 };
190 task.estimated_minutes = minutes;
191 }
192 TaskEditKey::ActualMinutes => {
193 let minutes = if trimmed.is_empty() {
194 None
195 } else {
196 Some(trimmed.parse::<u32>().with_context(|| {
197 format!(
198 "Queue edit failed (task_id={}, field=actual_minutes): must be a non-negative integer",
199 needle
200 )
201 })?)
202 };
203 task.actual_minutes = minutes;
204 }
205 }
206
207 if !matches!(key, TaskEditKey::UpdatedAt) {
208 let now = ensure_now(now_rfc3339)?;
209 task.updated_at = Some(now.to_string());
210 }
211
212 match queue::validate_queue_set(queue, done, id_prefix, id_width, max_dependency_depth) {
213 Ok(warnings) => {
214 queue::log_warnings(&warnings);
215 }
216 Err(err) => {
217 queue.tasks[index] = previous;
218 return Err(err);
219 }
220 }
221
222 Ok(())
223}