dev_scope/shared/models/internal/
doctor_group.rs

1use crate::models::prelude::{ModelMetadata, V1AlphaDoctorGroup};
2use crate::models::HelpMetadata;
3use crate::shared::models::internal::extract_command_path;
4use derive_builder::Builder;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, PartialEq, Clone, Builder)]
8#[builder(setter(into))]
9pub struct DoctorGroupAction {
10    pub name: String,
11    pub description: String,
12    pub fix: DoctorGroupActionFix,
13    pub check: DoctorGroupActionCheck,
14    pub required: bool,
15}
16
17#[derive(Debug, PartialEq, Clone, Builder)]
18#[builder(setter(into))]
19pub struct DoctorGroupActionFix {
20    #[builder(default)]
21    pub command: Option<DoctorGroupActionCommand>,
22    #[builder(default)]
23    pub help_text: Option<String>,
24    #[builder(default)]
25    pub help_url: Option<String>,
26}
27
28impl DoctorGroupAction {
29    pub fn make_from(
30        name: &str,
31        description: &str,
32        fix_command: Option<Vec<&str>>,
33        check_path: Option<(&str, Vec<&str>)>,
34        check_command: Option<Vec<&str>>,
35    ) -> Self {
36        Self {
37            required: true,
38            name: name.to_string(),
39            description: description.to_string(),
40            fix: DoctorGroupActionFix {
41                command: fix_command.map(DoctorGroupActionCommand::from),
42                help_text: None,
43                help_url: None,
44            },
45            check: DoctorGroupActionCheck {
46                command: check_command.map(DoctorGroupActionCommand::from),
47                files: check_path.map(|(base, paths)| DoctorGroupCachePath {
48                    base_path: PathBuf::from(base),
49                    paths: crate::shared::convert_to_string(paths),
50                }),
51            },
52        }
53    }
54}
55
56#[derive(Debug, PartialEq, Clone, Builder)]
57#[builder(setter(into))]
58pub struct DoctorGroupCachePath {
59    pub paths: Vec<String>,
60    pub base_path: PathBuf,
61}
62
63impl From<(&str, Vec<&str>)> for DoctorGroupCachePath {
64    fn from(value: (&str, Vec<&str>)) -> Self {
65        let pb = PathBuf::from(value.0);
66        let paths = crate::shared::convert_to_string(value.1);
67
68        Self {
69            paths,
70            base_path: pb,
71        }
72    }
73}
74
75#[derive(Debug, PartialEq, Clone, Builder)]
76#[builder(setter(into))]
77pub struct DoctorGroupActionCheck {
78    pub command: Option<DoctorGroupActionCommand>,
79    pub files: Option<DoctorGroupCachePath>,
80}
81
82#[derive(Debug, PartialEq, Clone, Builder)]
83#[builder(setter(into))]
84pub struct DoctorGroupActionCommand {
85    pub commands: Vec<String>,
86}
87
88impl From<Vec<&str>> for DoctorGroupActionCommand {
89    fn from(value: Vec<&str>) -> Self {
90        let commands = value.iter().map(|x| x.to_string()).collect();
91        Self { commands }
92    }
93}
94
95impl<T> From<(&Path, Vec<T>)> for DoctorGroupActionCommand
96where
97    String: for<'a> From<&'a T>,
98{
99    fn from((base_path, command_strings): (&Path, Vec<T>)) -> Self {
100        let commands = command_strings
101            .iter()
102            .map(|s| {
103                let exec: String = s.into();
104                extract_command_path(base_path, &exec)
105            })
106            .collect();
107
108        DoctorGroupActionCommand { commands }
109    }
110}
111
112#[derive(Debug, PartialEq, Clone, Builder)]
113#[builder(setter(into))]
114pub struct DoctorGroup {
115    pub full_name: String,
116    pub metadata: ModelMetadata,
117    pub requires: Vec<String>,
118    pub actions: Vec<DoctorGroupAction>,
119}
120
121impl HelpMetadata for DoctorGroup {
122    fn metadata(&self) -> &ModelMetadata {
123        &self.metadata
124    }
125
126    fn full_name(&self) -> String {
127        self.full_name.to_string()
128    }
129}
130
131impl TryFrom<V1AlphaDoctorGroup> for DoctorGroup {
132    type Error = anyhow::Error;
133
134    fn try_from(model: V1AlphaDoctorGroup) -> Result<Self, Self::Error> {
135        let binding = model.containing_dir();
136        let containing_dir = Path::new(&binding);
137        let mut actions: Vec<_> = Default::default();
138        for (count, spec_action) in model.spec.actions.iter().enumerate() {
139            let spec_action = spec_action.clone();
140            let help_text = spec_action
141                .fix
142                .as_ref()
143                .and_then(|x| x.help_text.as_ref().map(|st| st.trim().to_string()).clone());
144            let help_url = spec_action.fix.as_ref().and_then(|x| x.help_url.clone());
145            let fix_command = spec_action.fix.as_ref().map(|commands| {
146                DoctorGroupActionCommand::from((containing_dir, commands.commands.clone()))
147            });
148
149            actions.push(DoctorGroupAction {
150                name: spec_action.name.unwrap_or_else(|| format!("{}", count + 1)),
151                required: spec_action.required,
152                description: spec_action
153                    .description
154                    .unwrap_or_else(|| "default".to_string()),
155                fix: DoctorGroupActionFix {
156                    command: fix_command,
157                    help_text,
158                    help_url,
159                },
160                check: DoctorGroupActionCheck {
161                    command: spec_action
162                        .check
163                        .commands
164                        .map(|commands| DoctorGroupActionCommand::from((containing_dir, commands))),
165                    files: spec_action.check.paths.map(|paths| DoctorGroupCachePath {
166                        paths,
167                        base_path: containing_dir.parent().unwrap().to_path_buf(),
168                    }),
169                },
170            })
171        }
172
173        Ok(DoctorGroup {
174            full_name: model.full_name(),
175            metadata: model.metadata,
176            actions,
177            requires: model.spec.needs,
178        })
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use crate::shared::models::parse_models_from_string;
185    use crate::shared::models::prelude::{
186        DoctorGroupAction, DoctorGroupActionCheck, DoctorGroupActionCommand, DoctorGroupActionFix,
187    };
188    use crate::shared::prelude::DoctorGroupCachePath;
189
190    use std::path::Path;
191
192    #[test]
193    fn parse_group_1() {
194        let test_file = format!("{}/examples/group-1.yaml", env!("CARGO_MANIFEST_DIR"));
195        let text = std::fs::read_to_string(test_file).unwrap();
196        let path = Path::new("/foo/bar/.scope/file.yaml");
197        let configs = parse_models_from_string(path, &text).unwrap();
198        assert_eq!(1, configs.len());
199
200        let dg = configs[0].get_doctor_group().unwrap();
201        assert_eq!("foo", dg.metadata.name);
202        assert_eq!(
203            "/foo/bar/.scope/file.yaml",
204            dg.metadata.annotations.file_path.unwrap()
205        );
206        assert_eq!("/foo/bar/.scope", dg.metadata.annotations.file_dir.unwrap());
207        assert_eq!("ScopeDoctorGroup/foo", dg.full_name);
208        assert_eq!(vec!["bar"], dg.requires);
209
210        assert_eq!(
211            dg.actions[0],
212            DoctorGroupAction {
213                name: "1".to_string(),
214                required: false,
215                description: "foo1".to_string(),
216                fix: DoctorGroupActionFix {
217                    command: Some(DoctorGroupActionCommand::from(vec![
218                        "/foo/bar/.scope/fix1.sh"
219                    ])),
220                    help_text: Some("There is a good way to fix this, maybe...".to_string()),
221                    help_url: Some("https://go.example.com/fixit".to_string()),
222                },
223                check: DoctorGroupActionCheck {
224                    command: Some(DoctorGroupActionCommand::from(vec![
225                        "/foo/bar/.scope/foo1.sh"
226                    ])),
227                    files: Some(DoctorGroupCachePath::from((
228                        "/foo/bar",
229                        vec!["flig/bar/**/*"]
230                    )))
231                }
232            }
233        );
234        assert_eq!(
235            dg.actions[1],
236            DoctorGroupAction {
237                name: "2".to_string(),
238                required: true,
239                description: "foo2".to_string(),
240                fix: DoctorGroupActionFix {
241                    command: None,
242                    help_text: None,
243                    help_url: None,
244                },
245                check: DoctorGroupActionCheck {
246                    command: Some(DoctorGroupActionCommand::from(vec!["sleep infinity"])),
247                    files: Some(DoctorGroupCachePath::from(("/foo/bar", vec!["*/*.txt"])))
248                }
249            }
250        );
251    }
252}