Skip to main content

aven_core/
task_fields.rs

1use crate::ids::WorkspaceId;
2use anyhow::{Result, anyhow, ensure};
3use serde_json::{Value, json};
4
5use crate::choices::{TaskPriority, TaskStatus};
6use crate::types::Project;
7use crate::types::Task;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum TaskField {
11    Title,
12    Description,
13    Project,
14    Status,
15    Priority,
16    AvailableAt,
17    DueOn,
18    Deleted,
19    IsEpic,
20}
21
22impl TaskField {
23    pub const VERSIONED: [TaskField; 9] = [
24        TaskField::Title,
25        TaskField::Description,
26        TaskField::Project,
27        TaskField::Status,
28        TaskField::Priority,
29        TaskField::AvailableAt,
30        TaskField::Deleted,
31        TaskField::IsEpic,
32        TaskField::DueOn,
33    ];
34
35    pub fn parse(field: &str) -> Option<Self> {
36        match field {
37            "title" => Some(Self::Title),
38            "description" => Some(Self::Description),
39            "project" => Some(Self::Project),
40            "status" => Some(Self::Status),
41            "priority" => Some(Self::Priority),
42            "available_at" => Some(Self::AvailableAt),
43            "due_on" => Some(Self::DueOn),
44            "deleted" => Some(Self::Deleted),
45            "is_epic" => Some(Self::IsEpic),
46            _ => None,
47        }
48    }
49
50    pub fn is_project(self) -> bool {
51        matches!(self, Self::Project)
52    }
53
54    pub fn is_scalar(self) -> bool {
55        !self.is_project()
56    }
57
58    pub fn parse_or_unknown(field: &str) -> Result<Self> {
59        Self::parse(field).ok_or_else(|| anyhow!("error unknown-field field={field}"))
60    }
61
62    pub fn parse_for_sync(field: &str) -> Result<Self> {
63        Self::parse(field).ok_or_else(|| anyhow!("error invalid-sync-change field={field}"))
64    }
65
66    pub fn as_str(self) -> &'static str {
67        match self {
68            Self::Title => "title",
69            Self::Description => "description",
70            Self::Project => "project",
71            Self::Status => "status",
72            Self::Priority => "priority",
73            Self::AvailableAt => "available_at",
74            Self::DueOn => "due_on",
75            Self::Deleted => "deleted",
76            Self::IsEpic => "is_epic",
77        }
78    }
79
80    pub fn validate_value(self, value: &str) -> Result<()> {
81        match self {
82            Self::Status => TaskStatus::parse(value).map(|_| ()).map_err(Into::into),
83            Self::Priority => TaskPriority::parse(value).map(|_| ()).map_err(Into::into),
84            Self::AvailableAt => crate::time_validation::validate_available_at_value(value),
85            Self::DueOn => crate::time_validation::validate_due_on_value(value),
86            Self::Deleted if matches!(value, "0" | "1") => Ok(()),
87            Self::Deleted => anyhow::bail!("error invalid-deleted value={value}"),
88            Self::IsEpic if matches!(value, "0" | "1") => Ok(()),
89            Self::IsEpic => anyhow::bail!("error invalid-is-epic value={value}"),
90            _ => Ok(()),
91        }
92    }
93
94    pub fn updates_queue_activity(self) -> bool {
95        matches!(self, Self::Status | Self::Priority)
96    }
97
98    pub fn scalar_payload(
99        self,
100        workspace_id: &WorkspaceId,
101        workspace_key: &str,
102        value: &str,
103    ) -> Result<Value> {
104        ensure!(self.is_scalar(), "error project-update-requires-project-id");
105        Ok(json!({
106            "workspace_id": workspace_id,
107            "workspace_key": workspace_key,
108            "value": value,
109        }))
110    }
111
112    pub fn project_payload(
113        workspace_id: &WorkspaceId,
114        workspace_key: &str,
115        project: &Project,
116    ) -> Value {
117        json!({
118            "workspace_id": workspace_id,
119            "workspace_key": workspace_key,
120            "value": &project.id,
121            "project_id": &project.id,
122            "project_key": &project.key,
123            "project_name": &project.name,
124            "project_prefix": &project.prefix,
125        })
126    }
127
128    pub fn current_value(self, task: &Task) -> String {
129        match self {
130            Self::Title => task.title.clone(),
131            Self::Description => task.description.clone(),
132            Self::Project => task.project_id.to_string(),
133            Self::Status => task.status.as_str().to_string(),
134            Self::Priority => task.priority.as_str().to_string(),
135            Self::AvailableAt => task.available_at.clone().unwrap_or_default(),
136            Self::DueOn => task.due_on.clone().unwrap_or_default(),
137            Self::Deleted => {
138                if task.deleted {
139                    "1".to_string()
140                } else {
141                    "0".to_string()
142                }
143            }
144            Self::IsEpic => {
145                if task.is_epic {
146                    "1".to_string()
147                } else {
148                    "0".to_string()
149                }
150            }
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::TaskField;
158    use crate::ids::WorkspaceId;
159    use crate::types::Project;
160
161    #[test]
162    fn versioned_fields_keep_protocol_order() {
163        let fields: Vec<_> = TaskField::VERSIONED
164            .into_iter()
165            .map(TaskField::as_str)
166            .collect();
167        assert_eq!(
168            fields,
169            [
170                "title",
171                "description",
172                "project",
173                "status",
174                "priority",
175                "available_at",
176                "deleted",
177                "is_epic",
178                "due_on"
179            ]
180        );
181    }
182
183    #[test]
184    fn parse_unknown_field_errors_are_canonicalized() {
185        assert_eq!(
186            TaskField::parse_or_unknown("missing")
187                .unwrap_err()
188                .to_string(),
189            "error unknown-field field=missing"
190        );
191        assert_eq!(
192            TaskField::parse_for_sync("missing")
193                .unwrap_err()
194                .to_string(),
195            "error invalid-sync-change field=missing"
196        );
197    }
198
199    #[test]
200    fn deleted_validation() {
201        assert!(TaskField::Deleted.validate_value("0").is_ok());
202        assert!(TaskField::Deleted.validate_value("1").is_ok());
203        assert_eq!(
204            TaskField::Deleted
205                .validate_value("x")
206                .unwrap_err()
207                .to_string(),
208            "error invalid-deleted value=x"
209        );
210    }
211
212    #[test]
213    fn scalar_payload_shape() {
214        let workspace_id: WorkspaceId = "0000000000000001".parse().unwrap();
215        let payload = TaskField::Title
216            .scalar_payload(&workspace_id, "key", "v")
217            .expect("valid scalar field");
218        assert_eq!(
219            payload,
220            serde_json::json!({
221                "workspace_id": "0000000000000001",
222                "workspace_key": "key",
223                "value": "v",
224            })
225        );
226        assert!(
227            TaskField::Project
228                .scalar_payload(&workspace_id, "key", "v")
229                .is_err()
230        );
231    }
232
233    #[test]
234    fn project_payload_shape() {
235        let workspace_id: WorkspaceId = "0000000000000001".parse().unwrap();
236        let project = Project {
237            id: "0000000000000001".parse().unwrap(),
238            workspace_id: workspace_id.clone(),
239            key: "proj".to_string(),
240            name: "Project".to_string(),
241            prefix: "pp".to_string(),
242        };
243        assert_eq!(
244            TaskField::project_payload(&workspace_id, "key", &project),
245            serde_json::json!({
246                "workspace_id": "0000000000000001",
247                "workspace_key": "key",
248                "value": "0000000000000001",
249                "project_id": "0000000000000001",
250                "project_key": "proj",
251                "project_name": "Project",
252                "project_prefix": "pp",
253            })
254        );
255    }
256}