interprog/
lib.rs

1//! Inter-process progress reports.
2//!
3//! This module contains a `TaskManager` which you should instantiate once and reuse. It will schedule and output the tasks that you set to be running, queued, finished, and/or errored
4pub mod errors;
5
6use serde::{Deserialize, Serialize};
7use serde_json::ser::to_string as to_json_string;
8use std::collections::hash_map::Entry;
9use std::collections::HashMap;
10use std::io::{self, Write};
11/// Represents a task.
12#[derive(Serialize, Deserialize, Debug, PartialEq, Hash, Eq, Ord, PartialOrd)]
13pub struct Task {
14    /// The name of a task
15    pub name: String,
16    /// The current status of a task
17    /// Making this field flattened (so there's no `progress` key in the first place) with `#[serde(flatten)]` would
18    /// make it be annoying for other implementations to deserialize output in a type-safe manner
19    ///
20    /// ## Notes on the naming
21    /// - This field is named "progress" since the serialization will have a "status" field for the name.
22    /// - Naming this field `status` and renaming the current key for the status type to `type` (it's currently `status`) does not reflect that this whole key-value pair is about the progress of the task.
23    pub progress: Status,
24}
25impl Task {
26    pub fn new(name: impl Into<String>) -> Self {
27        Task {
28            name: name.into(),
29            progress: Status::Pending { total: None },
30        }
31    }
32    /// Change the total
33    ///
34    /// TODO: Subtasks
35    pub fn total(mut self, new_total: usize) -> Self {
36        self.progress = Status::Pending {
37            total: Some(new_total),
38        };
39        self
40    }
41    /// Change the name
42    pub fn name(mut self, new_name: impl Into<String>) -> Self {
43        self.name = new_name.into();
44        self
45    }
46}
47impl From<String> for Task {
48    fn from(name: String) -> Self {
49        Task::new(name)
50    }
51}
52/// Represents the status of a task.
53///
54/// There are 3 main states: Pending, Running, and finished.
55/// But since there are 2 types of running (iterative or spinner) and 3 types of finished (success and error), thus 5 variants
56#[derive(Serialize, Deserialize, Debug, PartialEq, Hash, Eq, Ord, PartialOrd)]
57#[serde(tag = "status")]
58pub enum Status {
59    /// Represents a pending task, waiting to be executed.
60    ///
61    /// The `total` field is optional. If it exists/is not null,
62    /// it means the task is *iterative* and has a known end
63    /// Otherwise, we assume it to be a spinner task.
64    /// The total field exists in this `Pending` variant since if we use, say
65    /// ```
66    /// # use interprog::Status;
67    /// # let X = 1;
68    /// Status::InProgress{done: 0, total: X};
69    /// ```
70    /// to represent a pending task with a known total instead of
71    /// ```
72    /// # use interprog::Status;
73    /// # let X = Some(1);
74    /// Status::Pending{total: X};
75    /// ```
76    /// it is ambiguous whether or not the task has already
77    /// started or not.
78    #[serde(rename = "pending")]
79    Pending { total: Option<usize> },
80    /// Self-explanatory
81    #[serde(rename = "error")]
82    Error { message: String },
83    /// Self-explanatory
84    #[serde(rename = "finished")]
85    Finished,
86    /// Like `InProgress` but for non-iterative tasks (unknown total)
87    #[serde(rename = "running")]
88    Running,
89    /// An **iterative task** (known end and/or subtasks)
90    /// is running.
91    ///
92    /// `done` out of `total` tasks were finished
93    /// The `subtasks` field is currently unused but will be
94    /// in the future when we implement nested tasks
95    /// TODO: implement subtasks
96    #[serde(rename = "in_progress")]
97    InProgress {
98        done: usize,
99        total: usize,
100        // subtasks: Option<TaskManager>,
101    },
102}
103/// The main struct that manages printing tasks
104///
105/// Most methods have an `task` variant that
106/// works on a specified task name instead of
107/// the first unfinished task (FIFO). This is to
108/// account for the future, when we
109/// actually support multithreading.
110/// Yes, this struct is currently *not thread-safe*
111/// (I think)
112#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
113pub struct TaskManager {
114    pub tasks: HashMap<String, Task>,
115    pub task_list: Vec<String>,
116    pub task_counter: usize,
117    silent: bool,
118}
119
120impl TaskManager {
121    #[inline]
122    fn output(&self) {
123        if self.silent {
124            return;
125        }
126        println!(
127            "{}",
128            to_json_string(&self.tasks.values().collect::<Vec<_>>()).expect("Should never happen")
129        );
130        io::stdout().flush().unwrap();
131    }
132    pub fn new() -> Self {
133        Self {
134            tasks: HashMap::new(),
135            task_list: Vec::new(),
136            task_counter: 0,
137            silent: false,
138        }
139    }
140
141    pub fn add_task(&mut self, task: Task) -> Result<(), errors::InterprogError> {
142        let name = task.name.clone();
143        match self.tasks.entry(name.clone()) {
144            Entry::Occupied(_) => return Err(errors::InterprogError::TaskAlreadyExists),
145            Entry::Vacant(entry) => entry.insert(task),
146        };
147        self.task_list.push(name);
148        Ok(())
149    }
150
151    pub fn start_task(&mut self, task_name: impl AsRef<str>) -> Result<(), errors::InterprogError> {
152        let task = &mut self
153            .tasks
154            .get_mut(task_name.as_ref())
155            .ok_or(errors::InterprogError::NonexistentTask)?;
156        if let Status::Pending { total } = &task.progress {
157            match total {
158                Some(total) => {
159                    task.progress = Status::InProgress {
160                        done: 0,
161                        total: *total,
162                        // subtasks: None,
163                    };
164                }
165                None => task.progress = Status::Running,
166            }
167        } else {
168            return Err(errors::InterprogError::TaskAlreadyStarted);
169        }
170        self.output();
171        Ok(())
172    }
173    pub fn start(&mut self) -> Result<(), errors::InterprogError> {
174        let task_name: String = self
175            .task_list
176            .get(self.task_counter)
177            .ok_or(errors::InterprogError::NonexistentTask)?
178            .clone();
179        self.start_task(&task_name)
180    }
181
182    pub fn increment_task(
183        &mut self,
184        task_name: impl AsRef<str>,
185        by: usize,
186    ) -> Result<(), errors::InterprogError> {
187        let task = &mut self
188            .tasks
189            .get_mut(task_name.as_ref())
190            .ok_or(errors::InterprogError::NonexistentTask)?;
191        // Never started before
192        match &task.progress {
193            Status::Pending { total: Some(total) } => {
194                task.progress = Status::InProgress {
195                    done: 1,
196                    total: *total,
197                    // subtasks: None,
198                };
199            }
200            Status::InProgress {
201                done,
202                total,
203                // subtasks: _,
204            } => {
205                if done >= total {
206                    return Err(errors::InterprogError::TaskAlreadyFinished);
207                }
208                // TODO: If incrementing makes it full, do we consider finished?
209                task.progress = Status::InProgress {
210                    done: done + by,
211                    total: *total,
212                    // subtasks: None,
213                };
214            }
215            Status::Running | Status::Pending { total: None } => {
216                return Err(errors::InterprogError::InvalidTaskType)
217            }
218            Status::Finished | Status::Error { message: _ } => {
219                return Err(errors::InterprogError::TaskAlreadyFinished)
220            }
221        }
222        self.output();
223        Ok(())
224    }
225    pub fn increment(&mut self, by: usize) -> Result<(), errors::InterprogError> {
226        let task_name: String = self
227            .task_list
228            .get(self.task_counter)
229            .ok_or(errors::InterprogError::NonexistentTask)?
230            .clone();
231        self.increment_task(&task_name, by)
232    }
233
234    pub fn finish_task(
235        &mut self,
236        task_name: impl AsRef<str>,
237    ) -> Result<(), errors::InterprogError> {
238        let task = &mut self
239            .tasks
240            .get_mut(task_name.as_ref())
241            .ok_or(errors::InterprogError::NonexistentTask)?;
242        // TODO: Implement subtasks
243        // if let Status::InProgress {
244        //     done: _,
245        //     total: _,
246        //     subtasks: Some(ref mut subtasks),
247        // } = task.progress
248        // {
249        //     for task in &subtasks.task_list.clone() {
250        //         subtasks.finish_task(&task);
251        //     }
252        // }
253
254        task.progress = Status::Finished;
255        self.task_counter += 1;
256        self.output();
257        Ok(())
258    }
259    pub fn finish(&mut self) -> Result<(), errors::InterprogError> {
260        let task_name: String = self
261            .task_list
262            .get(self.task_counter)
263            .ok_or(errors::InterprogError::NonexistentTask)?
264            .clone();
265        self.finish_task(&task_name)
266    }
267
268    pub fn error_task(
269        &mut self,
270        task_name: impl AsRef<str>,
271        message: impl Into<String>,
272    ) -> Result<(), errors::InterprogError> {
273        let task = &mut self
274            .tasks
275            .get_mut(task_name.as_ref())
276            .ok_or(errors::InterprogError::NonexistentTask)?;
277        task.progress = Status::Error {
278            message: message.into(),
279        };
280        self.task_counter += 1;
281        self.output();
282        Ok(())
283    }
284    pub fn error(&mut self, message: impl Into<String>) -> Result<(), errors::InterprogError> {
285        let task_name: String = self
286            .task_list
287            .get(self.task_counter)
288            .ok_or(errors::InterprogError::NonexistentTask)?
289            .clone();
290        self.error_task(&task_name, message)
291    }
292}
293impl Default for TaskManager {
294    fn default() -> Self {
295        Self::new()
296    }
297}
298#[cfg(test)]
299mod tests {
300    use crate::{Task, TaskManager};
301
302    #[test]
303    fn it_works() {
304        let mut manager = TaskManager::new();
305        manager.add_task(Task::new("name")).unwrap();
306        manager.start().unwrap();
307        manager.finish().unwrap();
308    }
309    #[test]
310    fn real_example() {
311        let mut manager = TaskManager::new();
312        manager.add_task(Task::new("Log in")).unwrap();
313        manager.start().unwrap();
314        manager.finish().unwrap();
315        let classes = vec!["English", "History", "Science", "Math"];
316        for class in &classes {
317            manager
318                .add_task(Task::new(format!("Scraping {class}")).total(4))
319                .unwrap();
320        }
321        for _ in 0..4 {
322            for class in &classes {
323                manager
324                    .increment_task(format!("Scraping {class}"), 1)
325                    .unwrap();
326            }
327        }
328    }
329    #[test]
330    fn static_names() {
331        let mut manager = TaskManager::new();
332        manager.add_task(Task::new("Log in")).unwrap();
333        manager.start_task("Log in").unwrap();
334        manager.finish().unwrap();
335    }
336}