Skip to main content

ralph/queue/operations/edit/
key.rs

1//! Task edit key definitions.
2//!
3//! Responsibilities:
4//! - Define the `TaskEditKey` enum representing editable task fields.
5//! - Provide string parsing and formatting for task edit keys.
6//!
7//! Does not handle:
8//! - Actual task editing logic (see `apply.rs` and `preview.rs`).
9//! - Input validation beyond key parsing.
10//!
11//! Assumptions/invariants:
12//! - TaskEditKey variants map 1:1 with Task struct fields.
13//! - String representations use snake_case for consistency.
14
15use crate::contracts::Task;
16use anyhow::{Result, bail};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum TaskEditKey {
20    Title,
21    Description,
22    Status,
23    Priority,
24    Tags,
25    Scope,
26    Evidence,
27    Plan,
28    Notes,
29    Request,
30    DependsOn,
31    Blocks,
32    RelatesTo,
33    Duplicates,
34    CustomFields,
35    Agent,
36    CreatedAt,
37    UpdatedAt,
38    CompletedAt,
39    StartedAt,
40    ScheduledStart,
41    EstimatedMinutes,
42    ActualMinutes,
43}
44
45impl TaskEditKey {
46    pub fn as_str(self) -> &'static str {
47        match self {
48            TaskEditKey::Title => "title",
49            TaskEditKey::Description => "description",
50            TaskEditKey::Status => "status",
51            TaskEditKey::Priority => "priority",
52            TaskEditKey::Tags => "tags",
53            TaskEditKey::Scope => "scope",
54            TaskEditKey::Evidence => "evidence",
55            TaskEditKey::Plan => "plan",
56            TaskEditKey::Notes => "notes",
57            TaskEditKey::Request => "request",
58            TaskEditKey::DependsOn => "depends_on",
59            TaskEditKey::Blocks => "blocks",
60            TaskEditKey::RelatesTo => "relates_to",
61            TaskEditKey::Duplicates => "duplicates",
62            TaskEditKey::CustomFields => "custom_fields",
63            TaskEditKey::Agent => "agent",
64            TaskEditKey::CreatedAt => "created_at",
65            TaskEditKey::UpdatedAt => "updated_at",
66            TaskEditKey::CompletedAt => "completed_at",
67            TaskEditKey::StartedAt => "started_at",
68            TaskEditKey::ScheduledStart => "scheduled_start",
69            TaskEditKey::EstimatedMinutes => "estimated_minutes",
70            TaskEditKey::ActualMinutes => "actual_minutes",
71        }
72    }
73
74    /// Returns whether this field is a list type (`Vec<String>`).
75    pub fn is_list_field(self) -> bool {
76        matches!(
77            self,
78            TaskEditKey::Tags
79                | TaskEditKey::Scope
80                | TaskEditKey::Evidence
81                | TaskEditKey::Plan
82                | TaskEditKey::Notes
83                | TaskEditKey::DependsOn
84                | TaskEditKey::Blocks
85                | TaskEditKey::RelatesTo
86        )
87    }
88
89    /// Format this field's value from a task with the given list separator.
90    ///
91    /// For list fields, elements are joined with the provided separator.
92    /// For optional fields, returns empty string when None.
93    pub fn format_value(self, task: &Task, list_sep: &str) -> String {
94        match self {
95            TaskEditKey::Title => task.title.clone(),
96            TaskEditKey::Description => task.description.clone().unwrap_or_default(),
97            TaskEditKey::Status => task.status.to_string(),
98            TaskEditKey::Priority => task.priority.to_string(),
99            TaskEditKey::Tags => task.tags.join(list_sep),
100            TaskEditKey::Scope => task.scope.join(list_sep),
101            TaskEditKey::Evidence => task.evidence.join(list_sep),
102            TaskEditKey::Plan => task.plan.join(list_sep),
103            TaskEditKey::Notes => task.notes.join(list_sep),
104            TaskEditKey::Request => task.request.clone().unwrap_or_default(),
105            TaskEditKey::DependsOn => task.depends_on.join(list_sep),
106            TaskEditKey::Blocks => task.blocks.join(list_sep),
107            TaskEditKey::RelatesTo => task.relates_to.join(list_sep),
108            TaskEditKey::Duplicates => task.duplicates.clone().unwrap_or_default(),
109            TaskEditKey::CustomFields => {
110                let pairs: Vec<String> = task
111                    .custom_fields
112                    .iter()
113                    .map(|(k, v)| format!("{}={}", k, v))
114                    .collect();
115                pairs.join(list_sep)
116            }
117            TaskEditKey::Agent => task
118                .agent
119                .as_ref()
120                .and_then(|agent| serde_json::to_string(agent).ok())
121                .unwrap_or_default(),
122            TaskEditKey::CreatedAt => task.created_at.clone().unwrap_or_default(),
123            TaskEditKey::UpdatedAt => task.updated_at.clone().unwrap_or_default(),
124            TaskEditKey::CompletedAt => task.completed_at.clone().unwrap_or_default(),
125            TaskEditKey::StartedAt => task.started_at.clone().unwrap_or_default(),
126            TaskEditKey::ScheduledStart => task.scheduled_start.clone().unwrap_or_default(),
127            TaskEditKey::EstimatedMinutes => task
128                .estimated_minutes
129                .map(|m| m.to_string())
130                .unwrap_or_default(),
131            TaskEditKey::ActualMinutes => task
132                .actual_minutes
133                .map(|m| m.to_string())
134                .unwrap_or_default(),
135        }
136    }
137}
138
139impl std::str::FromStr for TaskEditKey {
140    type Err = anyhow::Error;
141
142    fn from_str(value: &str) -> Result<Self> {
143        let normalized = value.trim().to_lowercase();
144        match normalized.as_str() {
145            "title" => Ok(TaskEditKey::Title),
146            "description" => Ok(TaskEditKey::Description),
147            "status" => Ok(TaskEditKey::Status),
148            "priority" => Ok(TaskEditKey::Priority),
149            "tags" => Ok(TaskEditKey::Tags),
150            "scope" => Ok(TaskEditKey::Scope),
151            "evidence" => Ok(TaskEditKey::Evidence),
152            "plan" => Ok(TaskEditKey::Plan),
153            "notes" => Ok(TaskEditKey::Notes),
154            "request" => Ok(TaskEditKey::Request),
155            "depends_on" => Ok(TaskEditKey::DependsOn),
156            "blocks" => Ok(TaskEditKey::Blocks),
157            "relates_to" => Ok(TaskEditKey::RelatesTo),
158            "duplicates" => Ok(TaskEditKey::Duplicates),
159            "custom_fields" => Ok(TaskEditKey::CustomFields),
160            "agent" => Ok(TaskEditKey::Agent),
161            "created_at" => Ok(TaskEditKey::CreatedAt),
162            "updated_at" => Ok(TaskEditKey::UpdatedAt),
163            "completed_at" => Ok(TaskEditKey::CompletedAt),
164            "started_at" => Ok(TaskEditKey::StartedAt),
165            "scheduled_start" => Ok(TaskEditKey::ScheduledStart),
166            "estimated_minutes" => Ok(TaskEditKey::EstimatedMinutes),
167            "actual_minutes" => Ok(TaskEditKey::ActualMinutes),
168            _ => bail!(
169                "Unknown task field: '{}'. Expected one of: title, description, status, priority, tags, scope, evidence, plan, notes, request, depends_on, blocks, relates_to, duplicates, custom_fields, agent, created_at, updated_at, completed_at, started_at, scheduled_start, estimated_minutes, actual_minutes.",
170                value
171            ),
172        }
173    }
174}