1use std::collections::BTreeMap;
6
7use async_trait::async_trait;
8
9use crate::{app::AppContext, errors::Error, Result};
10
11#[derive(Default, Debug)]
13pub struct Vars {
14 pub cli: BTreeMap<String, String>,
16}
17
18impl Vars {
19 #[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 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#[allow(clippy::module_name_repetitions)]
67#[derive(Debug)]
68pub struct TaskInfo {
69 pub name: String,
70 pub detail: String,
71}
72
73#[async_trait]
75pub trait Task: Send + Sync {
76 fn task(&self) -> TaskInfo;
78 async fn run(&self, app_context: &AppContext, vars: &Vars) -> Result<()>;
80}
81
82#[derive(Default)]
84pub struct Tasks {
85 registry: BTreeMap<String, Box<dyn Task>>,
86}
87
88impl Tasks {
89 #[must_use]
91 pub fn list(&self) -> Vec<TaskInfo> {
92 self.registry.values().map(|t| t.task()).collect::<Vec<_>>()
93 }
94
95 #[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 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 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 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 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 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 tasks.register(CustomFoo);
272
273 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}