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    Status,
22    Priority,
23    Tags,
24    Scope,
25    Evidence,
26    Plan,
27    Notes,
28    Request,
29    DependsOn,
30    Blocks,
31    RelatesTo,
32    Duplicates,
33    CustomFields,
34    Agent,
35    CreatedAt,
36    UpdatedAt,
37    CompletedAt,
38    StartedAt,
39    ScheduledStart,
40    EstimatedMinutes,
41    ActualMinutes,
42}
43
44impl TaskEditKey {
45    pub fn as_str(self) -> &'static str {
46        match self {
47            TaskEditKey::Title => "title",
48            TaskEditKey::Status => "status",
49            TaskEditKey::Priority => "priority",
50            TaskEditKey::Tags => "tags",
51            TaskEditKey::Scope => "scope",
52            TaskEditKey::Evidence => "evidence",
53            TaskEditKey::Plan => "plan",
54            TaskEditKey::Notes => "notes",
55            TaskEditKey::Request => "request",
56            TaskEditKey::DependsOn => "depends_on",
57            TaskEditKey::Blocks => "blocks",
58            TaskEditKey::RelatesTo => "relates_to",
59            TaskEditKey::Duplicates => "duplicates",
60            TaskEditKey::CustomFields => "custom_fields",
61            TaskEditKey::Agent => "agent",
62            TaskEditKey::CreatedAt => "created_at",
63            TaskEditKey::UpdatedAt => "updated_at",
64            TaskEditKey::CompletedAt => "completed_at",
65            TaskEditKey::StartedAt => "started_at",
66            TaskEditKey::ScheduledStart => "scheduled_start",
67            TaskEditKey::EstimatedMinutes => "estimated_minutes",
68            TaskEditKey::ActualMinutes => "actual_minutes",
69        }
70    }
71
72    /// Returns whether this field is a list type (`Vec<String>`).
73    pub fn is_list_field(self) -> bool {
74        matches!(
75            self,
76            TaskEditKey::Tags
77                | TaskEditKey::Scope
78                | TaskEditKey::Evidence
79                | TaskEditKey::Plan
80                | TaskEditKey::Notes
81                | TaskEditKey::DependsOn
82                | TaskEditKey::Blocks
83                | TaskEditKey::RelatesTo
84        )
85    }
86
87    /// Format this field's value from a task with the given list separator.
88    ///
89    /// For list fields, elements are joined with the provided separator.
90    /// For optional fields, returns empty string when None.
91    pub fn format_value(self, task: &Task, list_sep: &str) -> String {
92        match self {
93            TaskEditKey::Title => task.title.clone(),
94            TaskEditKey::Status => task.status.to_string(),
95            TaskEditKey::Priority => task.priority.to_string(),
96            TaskEditKey::Tags => task.tags.join(list_sep),
97            TaskEditKey::Scope => task.scope.join(list_sep),
98            TaskEditKey::Evidence => task.evidence.join(list_sep),
99            TaskEditKey::Plan => task.plan.join(list_sep),
100            TaskEditKey::Notes => task.notes.join(list_sep),
101            TaskEditKey::Request => task.request.clone().unwrap_or_default(),
102            TaskEditKey::DependsOn => task.depends_on.join(list_sep),
103            TaskEditKey::Blocks => task.blocks.join(list_sep),
104            TaskEditKey::RelatesTo => task.relates_to.join(list_sep),
105            TaskEditKey::Duplicates => task.duplicates.clone().unwrap_or_default(),
106            TaskEditKey::CustomFields => {
107                let pairs: Vec<String> = task
108                    .custom_fields
109                    .iter()
110                    .map(|(k, v)| format!("{}={}", k, v))
111                    .collect();
112                pairs.join(list_sep)
113            }
114            TaskEditKey::Agent => task
115                .agent
116                .as_ref()
117                .and_then(|agent| serde_json::to_string(agent).ok())
118                .unwrap_or_default(),
119            TaskEditKey::CreatedAt => task.created_at.clone().unwrap_or_default(),
120            TaskEditKey::UpdatedAt => task.updated_at.clone().unwrap_or_default(),
121            TaskEditKey::CompletedAt => task.completed_at.clone().unwrap_or_default(),
122            TaskEditKey::StartedAt => task.started_at.clone().unwrap_or_default(),
123            TaskEditKey::ScheduledStart => task.scheduled_start.clone().unwrap_or_default(),
124            TaskEditKey::EstimatedMinutes => task
125                .estimated_minutes
126                .map(|m| m.to_string())
127                .unwrap_or_default(),
128            TaskEditKey::ActualMinutes => task
129                .actual_minutes
130                .map(|m| m.to_string())
131                .unwrap_or_default(),
132        }
133    }
134}
135
136impl std::str::FromStr for TaskEditKey {
137    type Err = anyhow::Error;
138
139    fn from_str(value: &str) -> Result<Self> {
140        let normalized = value.trim().to_lowercase();
141        match normalized.as_str() {
142            "title" => Ok(TaskEditKey::Title),
143            "status" => Ok(TaskEditKey::Status),
144            "priority" => Ok(TaskEditKey::Priority),
145            "tags" => Ok(TaskEditKey::Tags),
146            "scope" => Ok(TaskEditKey::Scope),
147            "evidence" => Ok(TaskEditKey::Evidence),
148            "plan" => Ok(TaskEditKey::Plan),
149            "notes" => Ok(TaskEditKey::Notes),
150            "request" => Ok(TaskEditKey::Request),
151            "depends_on" => Ok(TaskEditKey::DependsOn),
152            "blocks" => Ok(TaskEditKey::Blocks),
153            "relates_to" => Ok(TaskEditKey::RelatesTo),
154            "duplicates" => Ok(TaskEditKey::Duplicates),
155            "custom_fields" => Ok(TaskEditKey::CustomFields),
156            "agent" => Ok(TaskEditKey::Agent),
157            "created_at" => Ok(TaskEditKey::CreatedAt),
158            "updated_at" => Ok(TaskEditKey::UpdatedAt),
159            "completed_at" => Ok(TaskEditKey::CompletedAt),
160            "started_at" => Ok(TaskEditKey::StartedAt),
161            "scheduled_start" => Ok(TaskEditKey::ScheduledStart),
162            "estimated_minutes" => Ok(TaskEditKey::EstimatedMinutes),
163            "actual_minutes" => Ok(TaskEditKey::ActualMinutes),
164            _ => bail!(
165                "Unknown task field: '{}'. Expected one of: title, 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.",
166                value
167            ),
168        }
169    }
170}