by_loco/
task.rs

1//! # Task Management Module
2//!
3//! This module defines the task management framework used to manage and execute
4//! tasks in a web server application.
5use std::collections::BTreeMap;
6
7use async_trait::async_trait;
8
9use crate::{app::AppContext, errors::Error, Result};
10
11/// Struct representing a collection of task arguments.
12#[derive(Default, Debug)]
13pub struct Vars {
14    /// A list of cli arguments.
15    pub cli: BTreeMap<String, String>,
16}
17
18impl Vars {
19    /// Create [`Vars`] instance from cli arguments.
20    ///
21    /// # Arguments
22    ///
23    /// * `key` - A string representing the key.
24    /// * `value` - A string representing the value.
25    ///
26    /// # Example
27    ///
28    /// ```
29    /// use loco_rs::task::Vars;
30    ///
31    /// let args = vec![("key1".to_string(), "value".to_string())];
32    /// let vars = Vars::from_cli_args(args);
33    /// ```
34    #[must_use]
35    pub fn from_cli_args(args: Vec<(String, String)>) -> Self {
36        Self {
37            cli: args.into_iter().collect(),
38        }
39    }
40
41    /// Retrieves the value associated with the given key from the `cli` list.
42    ///
43    /// # Errors
44    ///
45    /// Returns an error if the key does not exist.
46    ///
47    /// # Example
48    ///
49    /// ```
50    /// use loco_rs::task::Vars;
51    ///
52    /// let args = vec![("key1".to_string(), "value".to_string())];
53    /// let vars = Vars::from_cli_args(args);
54    ///
55    /// assert!(vars.cli_arg("key1").is_ok());
56    /// assert!(vars.cli_arg("not-exists").is_err());
57    /// ```
58    pub fn cli_arg(&self, key: &str) -> Result<&String> {
59        self.cli
60            .get(key)
61            .ok_or(Error::Message(format!("the argument {key} does not exist")))
62    }
63}
64
65/// Information about a task, including its name and details.
66#[allow(clippy::module_name_repetitions)]
67#[derive(Debug)]
68pub struct TaskInfo {
69    pub name: String,
70    pub detail: String,
71}
72
73/// A trait defining the behavior of a task.
74#[async_trait]
75pub trait Task: Send + Sync {
76    /// Get information about the task.
77    fn task(&self) -> TaskInfo;
78    /// Execute the task with the provided application context and variables.
79    async fn run(&self, app_context: &AppContext, vars: &Vars) -> Result<()>;
80}
81
82/// Managing and running tasks.
83#[derive(Default)]
84pub struct Tasks {
85    registry: BTreeMap<String, Box<dyn Task>>,
86}
87
88impl Tasks {
89    /// List all registered tasks with their information.
90    #[must_use]
91    pub fn list(&self) -> Vec<TaskInfo> {
92        self.registry.values().map(|t| t.task()).collect::<Vec<_>>()
93    }
94
95    /// List of all tasks names
96    #[must_use]
97    pub fn names(&self) -> Vec<String> {
98        self.registry
99            .values()
100            .map(|t| t.task().name)
101            .collect::<Vec<_>>()
102    }
103
104    /// Run a registered task by name with provided variables.
105    ///
106    /// # Errors
107    ///
108    /// Returns a [`Result`] if an task finished with error. mostly if the given
109    /// task is not found or an error to run the task.s
110    pub async fn run(&self, app_context: &AppContext, task: &str, vars: &Vars) -> Result<()> {
111        let task = self
112            .registry
113            .get(task)
114            .ok_or_else(|| Error::TaskNotFound(task.to_string()))?;
115        task.run(app_context, vars).await?;
116        Ok(())
117    }
118
119    /// Register a new task to the registry.
120    pub fn register(&mut self, task: impl Task + 'static) {
121        let name = task.task().name;
122        self.registry.insert(name, Box::new(task));
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::tests_cfg;
130
131    #[tokio::test]
132    async fn test_vars_from_cli_args() {
133        let args = vec![
134            ("key1".to_string(), "value1".to_string()),
135            ("key2".to_string(), "value2".to_string()),
136        ];
137        let vars = Vars::from_cli_args(args);
138
139        assert_eq!(vars.cli.len(), 2);
140        assert_eq!(vars.cli.get("key1"), Some(&"value1".to_string()));
141        assert_eq!(vars.cli.get("key2"), Some(&"value2".to_string()));
142    }
143
144    #[tokio::test]
145    async fn test_vars_cli_arg() {
146        let args = vec![("key1".to_string(), "value1".to_string())];
147        let vars = Vars::from_cli_args(args);
148
149        assert_eq!(vars.cli_arg("key1").unwrap(), "value1");
150        assert!(vars.cli_arg("not-exists").is_err());
151    }
152
153    #[tokio::test]
154    async fn test_tasks_registry() {
155        let mut tasks = Tasks::default();
156        tasks.register(tests_cfg::task::Foo);
157        tasks.register(tests_cfg::task::ParseArgs);
158
159        assert_eq!(tasks.names().len(), 2);
160        assert!(tasks.names().contains(&"foo".to_string()));
161        assert!(tasks.names().contains(&"parse_args".to_string()));
162    }
163
164    #[tokio::test]
165    async fn test_tasks_list() {
166        let mut tasks = Tasks::default();
167        tasks.register(tests_cfg::task::Foo);
168        tasks.register(tests_cfg::task::ParseArgs);
169
170        let task_infos = tasks.list();
171        assert_eq!(task_infos.len(), 2);
172
173        let names: Vec<String> = task_infos.iter().map(|info| info.name.clone()).collect();
174        let details: Vec<String> = task_infos.iter().map(|info| info.detail.clone()).collect();
175
176        assert!(names.contains(&"foo".to_string()));
177        assert!(names.contains(&"parse_args".to_string()));
178        assert!(details.contains(&"run foo task".to_string()));
179        assert!(details.contains(&"Validate the paring args".to_string()));
180    }
181
182    #[tokio::test]
183    async fn test_tasks_run_success() {
184        let mut tasks = Tasks::default();
185        tasks.register(tests_cfg::task::Foo);
186
187        let app_context = tests_cfg::app::get_app_context().await;
188        let vars = Vars::default();
189
190        let result = tasks.run(&app_context, "foo", &vars).await;
191        assert!(result.is_ok());
192    }
193
194    #[tokio::test]
195    async fn test_tasks_run_failure() {
196        let mut tasks = Tasks::default();
197        tasks.register(tests_cfg::task::ParseArgs);
198
199        let app_context = tests_cfg::app::get_app_context().await;
200        let vars = Vars::default();
201
202        // ParseArgs will fail with "invalid args" if app != "loco" or refresh != true
203        let result = tasks.run(&app_context, "parse_args", &vars).await;
204        assert!(result.is_err());
205
206        if let Err(Error::Message(msg)) = result {
207            assert_eq!(msg, "invalid args");
208        } else {
209            panic!("Expected Error::Message variant");
210        }
211    }
212
213    #[tokio::test]
214    async fn test_tasks_run_with_args() {
215        let mut tasks = Tasks::default();
216        tasks.register(tests_cfg::task::ParseArgs);
217
218        let app_context = tests_cfg::app::get_app_context().await;
219        let args = vec![
220            ("test".to_string(), "true".to_string()),
221            ("app".to_string(), "loco".to_string()),
222        ];
223        let vars = Vars::from_cli_args(args);
224
225        // ParseArgs will succeed when app == "loco" and test == "true"
226        let result = tasks.run(&app_context, "parse_args", &vars).await;
227        assert!(result.is_ok());
228    }
229
230    #[tokio::test]
231    async fn test_tasks_run_not_found() {
232        let tasks = Tasks::default();
233        let app_context = tests_cfg::app::get_app_context().await;
234        let vars = Vars::default();
235
236        let result = tasks.run(&app_context, "non_existent_task", &vars).await;
237        assert!(result.is_err());
238
239        match result {
240            Err(Error::TaskNotFound(task_name)) => {
241                assert_eq!(task_name, "non_existent_task");
242            }
243            _ => panic!("Expected Error::TaskNotFound variant"),
244        }
245    }
246
247    #[tokio::test]
248    async fn test_task_registration_and_override() {
249        // Create a custom task that will override Foo
250        struct CustomFoo;
251
252        #[async_trait]
253        impl Task for CustomFoo {
254            fn task(&self) -> TaskInfo {
255                TaskInfo {
256                    name: "foo".to_string(),
257                    detail: "Updated foo task".to_string(),
258                }
259            }
260
261            async fn run(&self, _app_context: &AppContext, _vars: &Vars) -> Result<()> {
262                Ok(())
263            }
264        }
265
266        let mut tasks = Tasks::default();
267        tasks.register(tests_cfg::task::Foo);
268        assert_eq!(tasks.names().len(), 1);
269
270        // Register a new task with the same name
271        tasks.register(CustomFoo);
272
273        // Should still have only one task (overwritten)
274        assert_eq!(tasks.names().len(), 1);
275
276        let task_infos = tasks.list();
277        assert_eq!(task_infos[0].detail, "Updated foo task");
278    }
279}