1use std::path::PathBuf;
4
5use crate::errors::CoreError;
6use ito_domain::errors::DomainError;
7use ito_domain::tasks::{
8 TaskInitResult, TaskMutationError, TaskMutationResult, TaskMutationService,
9 TaskMutationServiceResult,
10};
11
12#[derive(Debug, Clone)]
14pub struct FsTaskMutationService {
15 ito_path: PathBuf,
16}
17
18impl FsTaskMutationService {
19 pub fn new(ito_path: impl Into<PathBuf>) -> Self {
21 Self {
22 ito_path: ito_path.into(),
23 }
24 }
25
26 fn missing_tasks_error(change_id: &str) -> TaskMutationError {
28 TaskMutationError::not_found(format!(
29 "No backend tasks found for \"{change_id}\". Run \"ito tasks init {change_id}\" first."
30 ))
31 }
32
33 fn require_tasks_path(&self, change_id: &str) -> TaskMutationServiceResult<std::path::PathBuf> {
38 let path = crate::tasks::tracking_file_path(&self.ito_path, change_id)
39 .map_err(task_mutation_error_from_core)?;
40 if !path.exists() {
41 return Err(Self::missing_tasks_error(change_id));
42 }
43 Ok(path)
44 }
45}
46
47impl TaskMutationService for FsTaskMutationService {
48 fn load_tasks_markdown(&self, change_id: &str) -> TaskMutationServiceResult<Option<String>> {
49 let path = crate::tasks::tracking_file_path(&self.ito_path, change_id)
50 .map_err(task_mutation_error_from_core)?;
51 if !path.exists() {
52 return Ok(None);
53 }
54 let contents = ito_common::io::read_to_string_std(&path)
55 .map_err(|e| TaskMutationError::io("reading tasks markdown", e))?;
56 Ok(Some(contents))
57 }
58
59 fn init_tasks(&self, change_id: &str) -> TaskMutationServiceResult<TaskInitResult> {
60 let (path, existed) = crate::tasks::init_tasks(&self.ito_path, change_id)
61 .map_err(task_mutation_error_from_core)?;
62 Ok(TaskInitResult {
63 change_id: change_id.to_string(),
64 path: Some(path),
65 existed,
66 revision: None,
67 })
68 }
69
70 fn start_task(
71 &self,
72 change_id: &str,
73 task_id: &str,
74 ) -> TaskMutationServiceResult<TaskMutationResult> {
75 let _ = self.require_tasks_path(change_id)?;
77 let task = crate::tasks::start_task(&self.ito_path, change_id, task_id)
78 .map_err(task_mutation_error_from_core)?;
79 Ok(TaskMutationResult {
80 change_id: change_id.to_string(),
81 task,
82 revision: None,
83 })
84 }
85
86 fn complete_task(
87 &self,
88 change_id: &str,
89 task_id: &str,
90 note: Option<String>,
91 ) -> TaskMutationServiceResult<TaskMutationResult> {
92 let _ = self.require_tasks_path(change_id)?;
93 let task = crate::tasks::complete_task(&self.ito_path, change_id, task_id, note)
94 .map_err(task_mutation_error_from_core)?;
95 Ok(TaskMutationResult {
96 change_id: change_id.to_string(),
97 task,
98 revision: None,
99 })
100 }
101
102 fn shelve_task(
103 &self,
104 change_id: &str,
105 task_id: &str,
106 reason: Option<String>,
107 ) -> TaskMutationServiceResult<TaskMutationResult> {
108 let _ = self.require_tasks_path(change_id)?;
109 let task = crate::tasks::shelve_task(&self.ito_path, change_id, task_id, reason)
110 .map_err(task_mutation_error_from_core)?;
111 Ok(TaskMutationResult {
112 change_id: change_id.to_string(),
113 task,
114 revision: None,
115 })
116 }
117
118 fn unshelve_task(
119 &self,
120 change_id: &str,
121 task_id: &str,
122 ) -> TaskMutationServiceResult<TaskMutationResult> {
123 let _ = self.require_tasks_path(change_id)?;
124 let task = crate::tasks::unshelve_task(&self.ito_path, change_id, task_id)
125 .map_err(task_mutation_error_from_core)?;
126 Ok(TaskMutationResult {
127 change_id: change_id.to_string(),
128 task,
129 revision: None,
130 })
131 }
132
133 fn add_task(
134 &self,
135 change_id: &str,
136 title: &str,
137 wave: Option<u32>,
138 ) -> TaskMutationServiceResult<TaskMutationResult> {
139 let _ = self.require_tasks_path(change_id)?;
140 let task = crate::tasks::add_task(&self.ito_path, change_id, title, wave)
141 .map_err(task_mutation_error_from_core)?;
142 Ok(TaskMutationResult {
143 change_id: change_id.to_string(),
144 task,
145 revision: None,
146 })
147 }
148}
149
150#[cfg(feature = "backend")]
151pub(crate) fn boxed_fs_task_mutation_service(
152 ito_path: PathBuf,
153) -> Box<dyn TaskMutationService + Send> {
154 Box::new(FsTaskMutationService::new(ito_path))
155}
156
157pub(crate) fn task_mutation_error_from_core(err: CoreError) -> TaskMutationError {
158 match err {
159 CoreError::Domain(domain) => match domain {
160 DomainError::Io { context, source } => TaskMutationError::io(context, source),
161 DomainError::NotFound { entity, id } => {
162 TaskMutationError::not_found(format!("{entity} not found: {id}"))
163 }
164 DomainError::AmbiguousTarget {
165 entity,
166 input,
167 matches,
168 } => TaskMutationError::validation(format!(
169 "Ambiguous {entity} target '{input}'. Matches: {matches}"
170 )),
171 },
172 CoreError::Io { context, source } => TaskMutationError::io(context, source),
173 CoreError::Validation(message) => TaskMutationError::validation(message),
174 CoreError::Parse(message) => TaskMutationError::validation(message),
175 CoreError::Process(message) => TaskMutationError::other(message),
176 CoreError::Sqlite(message) => TaskMutationError::other(format!("sqlite error: {message}")),
177 CoreError::NotFound(message) => TaskMutationError::not_found(message),
178 CoreError::Serde { context, message } => {
179 TaskMutationError::other(format!("{context}: {message}"))
180 }
181 error @ CoreError::FeatureUnavailable { .. } => TaskMutationError::other(error.to_string()),
182 }
183}