Skip to content

Commit 0731aaf

Browse files
Extract more code we'll need to implement known error fixes (#189)
Follow up to #188 in support of #160 We need to be able to properly translate from a fix spec to a fix in the `known-error` code. This extracts the additional code we'll need to be able to do so. Also improves test coverage
1 parent b18cc3b commit 0731aaf

File tree

4 files changed

+190
-45
lines changed

4 files changed

+190
-45
lines changed

scope/src/shared/models/internal/command.rs

+39-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
1-
use std::path::Path;
2-
1+
use anyhow::Result;
32
use derive_builder::Builder;
3+
use std::path::Path;
44

5-
use super::extract_command_path;
5+
use super::{extract_command_path, substitute_templates};
66

77
#[derive(Debug, PartialEq, Clone, Builder)]
88
#[builder(setter(into))]
99
pub struct DoctorCommand {
1010
pub commands: Vec<String>,
1111
}
1212

13+
impl DoctorCommand {
14+
pub fn from_commands(
15+
containing_dir: &Path,
16+
working_dir: &str,
17+
commands: &Vec<String>,
18+
) -> Result<DoctorCommand> {
19+
let mut templated_commands = Vec::new();
20+
for command in commands {
21+
templated_commands.push(substitute_templates(working_dir, command)?);
22+
}
23+
Ok(DoctorCommand::from((containing_dir, templated_commands)))
24+
}
25+
}
26+
1327
impl<T> From<(&Path, Vec<T>)> for DoctorCommand
1428
where
1529
String: for<'a> From<&'a T>,
@@ -74,4 +88,26 @@ mod tests {
7488
actual
7589
)
7690
}
91+
92+
#[test]
93+
fn from_commands_succeeds() {
94+
let containing_dir = Path::new("/foo/bar");
95+
let working_dir = "/some/working_dir";
96+
let commands = vec!["{{ working_dir }}/foo.sh", "./bar.sh"]
97+
.iter()
98+
.map(|cmd| cmd.to_string())
99+
.collect::<Vec<String>>();
100+
101+
let actual = DoctorCommand::from_commands(containing_dir, working_dir, &commands)
102+
.expect("Expected Ok");
103+
104+
let templated_commands = commands
105+
.iter()
106+
.map(|cmd| substitute_templates(&working_dir, &cmd).unwrap())
107+
.collect::<Vec<String>>();
108+
109+
let expected = DoctorCommand::from((containing_dir, templated_commands));
110+
111+
assert_eq!(expected, actual)
112+
}
77113
}

scope/src/shared/models/internal/doctor_group.rs

+15-41
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ use std::path::{Path, PathBuf};
33

44
use anyhow::Result;
55
use derive_builder::Builder;
6-
use minijinja::{context, Environment};
76

87
use crate::models::prelude::{ModelMetadata, V1AlphaDoctorGroup};
98
use crate::models::HelpMetadata;
109
use crate::prelude::{DoctorGroupActionSpec, DoctorInclude};
11-
use crate::shared::models::internal::{DoctorCommand, DoctorFix, DoctorFixPrompt};
10+
use crate::shared::models::internal::{DoctorCommand, DoctorFix};
11+
12+
use super::substitute_templates;
1213

1314
#[derive(Debug, PartialEq, Clone, Builder)]
1415
#[builder(setter(into))]
@@ -67,15 +68,6 @@ impl HelpMetadata for DoctorGroup {
6768
}
6869
}
6970

70-
fn substitute_templates(work_dir: &str, input_str: &str) -> Result<String> {
71-
let mut env = Environment::new();
72-
env.add_template("input_str", input_str)?;
73-
let template = env.get_template("input_str")?;
74-
let result = template.render(context! { working_dir => work_dir })?;
75-
76-
Ok(result)
77-
}
78-
7971
impl TryFrom<V1AlphaDoctorGroup> for DoctorGroup {
8072
type Error = anyhow::Error;
8173

@@ -112,29 +104,19 @@ fn parse_action(
112104
.clone();
113105

114106
let spec_action = action.clone();
115-
let help_text = spec_action
116-
.fix
117-
.as_ref()
118-
.and_then(|x| x.help_text.as_ref().map(|st| st.trim().to_string()).clone());
119-
let help_url = spec_action.fix.as_ref().and_then(|x| x.help_url.clone());
120-
let fix_command = if let Some(fix) = &spec_action.fix {
121-
let mut templated_commands = Vec::new();
122-
for command in &fix.commands {
123-
templated_commands.push(substitute_templates(&working_dir, command)?);
124-
}
125-
Some(DoctorCommand::from((containing_dir, templated_commands)))
126-
} else {
127-
None
107+
108+
let fix = match &spec_action.fix {
109+
Some(fix_spec) => DoctorFix::from_spec(containing_dir, &working_dir, fix_spec.clone())?,
110+
None => DoctorFix::empty(),
128111
};
129112

130-
let check_command = if let Some(ref check) = spec_action.check.commands {
131-
let mut templated_commands = Vec::new();
132-
for command in check {
133-
templated_commands.push(substitute_templates(&working_dir, command)?);
134-
}
135-
Some(DoctorCommand::from((containing_dir, templated_commands)))
136-
} else {
137-
None
113+
let check_command = match spec_action.check.commands {
114+
Some(ref check) => Some(DoctorCommand::from_commands(
115+
containing_dir,
116+
&working_dir,
117+
check,
118+
)?),
119+
None => None,
138120
};
139121

140122
Ok(DoctorGroupAction {
@@ -143,15 +125,7 @@ fn parse_action(
143125
description: spec_action
144126
.description
145127
.unwrap_or_else(|| "default".to_string()),
146-
fix: DoctorFix {
147-
command: fix_command,
148-
prompt: spec_action
149-
.fix
150-
.and_then(|fix| fix.prompt)
151-
.map(DoctorFixPrompt::from),
152-
help_text,
153-
help_url,
154-
},
128+
fix,
155129
check: DoctorGroupActionCheck {
156130
command: check_command,
157131
files: spec_action.check.paths.map(|paths| DoctorGroupCachePath {

scope/src/shared/models/internal/fix.rs

+89-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
use crate::{prelude::DoctorFixPromptSpec, shared::prelude::*};
1+
use anyhow::Result;
2+
use std::path::Path;
3+
4+
use crate::{
5+
prelude::{DoctorFixPromptSpec, DoctorFixSpec},
6+
shared::prelude::*,
7+
};
28
use derive_builder::Builder;
39

410
#[derive(Debug, PartialEq, Clone, Builder)]
@@ -14,6 +20,35 @@ pub struct DoctorFix {
1420
pub prompt: Option<DoctorFixPrompt>,
1521
}
1622

23+
impl DoctorFix {
24+
pub fn empty() -> Self {
25+
DoctorFix {
26+
command: None,
27+
help_text: None,
28+
help_url: None,
29+
prompt: None,
30+
}
31+
}
32+
33+
pub fn from_spec(containing_dir: &Path, working_dir: &str, fix: DoctorFixSpec) -> Result<Self> {
34+
let commands = DoctorCommand::from_commands(containing_dir, working_dir, &fix.commands)?;
35+
let help_text = fix
36+
.help_text
37+
.as_ref()
38+
.map(|st| st.trim().to_string())
39+
.clone();
40+
let help_url = fix.help_url.clone();
41+
let prompt = fix.prompt.map(DoctorFixPrompt::from);
42+
43+
Ok(DoctorFix {
44+
command: Some(commands),
45+
help_text,
46+
help_url,
47+
prompt,
48+
})
49+
}
50+
}
51+
1752
#[derive(Debug, PartialEq, Clone, Builder)]
1853
#[builder(setter(into))]
1954
pub struct DoctorFixPrompt {
@@ -53,4 +88,57 @@ mod tests {
5388
actual
5489
)
5590
}
91+
92+
#[test]
93+
fn empty_returns_a_fix_full_of_none() {
94+
// I can argue that we should use Option<DoctorFix> instead,
95+
// but for now, this is where we're at.
96+
assert_eq!(
97+
DoctorFix {
98+
command: None,
99+
help_text: None,
100+
help_url: None,
101+
prompt: None,
102+
},
103+
DoctorFix::empty()
104+
)
105+
}
106+
107+
#[test]
108+
fn from_spec_translates_to_fix() {
109+
let spec = DoctorFixSpec {
110+
commands: vec![
111+
"some/command",
112+
"./other_command",
113+
"{{ working_dir }}/.foo.sh",
114+
]
115+
.iter()
116+
.map(|cmd| cmd.to_string())
117+
.collect(),
118+
help_text: Some("text".to_string()),
119+
help_url: Some("https.example.com".to_string()),
120+
prompt: Some(DoctorFixPromptSpec {
121+
text: "do you want to do the thing?".to_string(),
122+
extra_context: Some("additional context".to_string()),
123+
}),
124+
};
125+
126+
let expected = DoctorFix {
127+
command: Some(
128+
DoctorCommand::from_commands(
129+
Path::new("/some/dir"),
130+
"/some/work/dir",
131+
&spec.commands,
132+
)
133+
.unwrap(),
134+
),
135+
help_text: spec.help_text.clone(),
136+
help_url: spec.help_url.clone(),
137+
prompt: Some(DoctorFixPrompt::from(spec.prompt.clone().unwrap())),
138+
};
139+
140+
let actual = DoctorFix::from_spec(Path::new("/some/dir"), "/some/work/dir", spec).unwrap();
141+
142+
assert_eq!(expected, actual)
143+
}
56144
}

scope/src/shared/models/internal/mod.rs

+47
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use crate::models::prelude::{
44
use crate::models::InternalScopeModel;
55
use crate::shared::prelude::*;
66
use anyhow::anyhow;
7+
use anyhow::Result;
8+
use minijinja::{context, Environment};
79
use path_clean::PathClean;
810
use serde_yaml::Value;
911
use std::collections::VecDeque;
@@ -91,6 +93,15 @@ pub(crate) fn extract_command_path(parent_dir: &Path, exec: &str) -> String {
9193
.join(" ")
9294
}
9395

96+
pub(crate) fn substitute_templates(work_dir: &str, input_str: &str) -> Result<String> {
97+
let mut env = Environment::new();
98+
env.add_template("input_str", input_str)?;
99+
let template = env.get_template("input_str")?;
100+
let result = template.render(context! { working_dir => work_dir })?;
101+
102+
Ok(result)
103+
}
104+
94105
#[cfg(test)]
95106
mod tests {
96107
use super::*;
@@ -109,4 +120,40 @@ mod tests {
109120
assert_eq!("foo", extract_command_path(base_path, "foo"));
110121
assert_eq!("foo bar", extract_command_path(base_path, "foo bar"));
111122
}
123+
124+
mod substitute_templates_spec {
125+
use super::*;
126+
127+
#[test]
128+
fn working_dir_is_subbed() {
129+
let working_dir = "/some/path";
130+
let command = "{{ working_dir }}/foo.sh";
131+
132+
let actual = substitute_templates(&working_dir, &command).unwrap();
133+
134+
assert_eq!("/some/path/foo.sh".to_string(), actual)
135+
}
136+
137+
#[test]
138+
fn text_without_templates_is_passed_through() {
139+
let working_dir = "/some/path";
140+
let command = "./foo.sh";
141+
142+
let actual = substitute_templates(&working_dir, &command).unwrap();
143+
144+
assert_eq!("./foo.sh".to_string(), actual)
145+
}
146+
147+
#[test]
148+
fn other_templates_are_erased() {
149+
// I don't believe this is intentional behavior,
150+
// but it is the current behavior.
151+
let working_dir = "/some/path";
152+
let command = "{{ not_a_thing }}/foo.sh";
153+
154+
let actual = substitute_templates(&working_dir, &command).unwrap();
155+
156+
assert_eq!("/foo.sh".to_string(), actual)
157+
}
158+
}
112159
}

0 commit comments

Comments
 (0)