openjd_model/template/
step.rs1use super::actions::{Action, CancelationMode, StepActions};
8use super::constrained_strings::Description;
9use super::environment::{EmbeddedFile, Environment};
10use super::host_requirements::HostRequirements;
11use super::task_parameters::StepParameterSpaceDefinition;
12use crate::format_string::FormatString;
13use serde::Deserialize;
14
15#[derive(Debug, Clone, Deserialize)]
18#[serde(rename_all = "camelCase", deny_unknown_fields)]
19pub struct SimpleAction {
20 #[serde(rename = "let")]
22 pub let_bindings: Option<Vec<String>>,
23 pub script: String,
25 pub args: Option<Vec<FormatString>>,
27 pub timeout: Option<FormatString>,
29 pub cancelation: Option<CancelationMode>,
31}
32
33#[derive(Debug, Clone, Deserialize)]
35#[serde(rename_all = "camelCase", deny_unknown_fields)]
36pub struct StepTemplate {
37 pub name: String,
38 pub description: Option<Description>,
39 #[serde(rename = "let")]
40 pub let_bindings: Option<Vec<String>>,
41 pub dependencies: Option<Vec<StepDependency>>,
42 pub step_environments: Option<Vec<Environment>>,
43 pub host_requirements: Option<HostRequirements>,
44 pub parameter_space: Option<StepParameterSpaceDefinition>,
45 pub script: Option<StepScript>,
46 pub bash: Option<SimpleAction>,
48 pub python: Option<SimpleAction>,
49 pub cmd: Option<SimpleAction>,
50 pub powershell: Option<SimpleAction>,
51 pub node: Option<SimpleAction>,
52}
53
54impl StepTemplate {
55 pub fn resolve_syntax_sugar(&self) -> Result<Option<StepScript>, crate::ModelError> {
61 if let Some(script) = &self.script {
62 return Ok(Some(script.clone()));
63 }
64
65 let interpreters: &[(&str, &str, &[&str], Option<&SimpleAction>)] = &[
66 ("python", ".py", &[], self.python.as_ref()),
67 ("bash", ".sh", &[], self.bash.as_ref()),
68 ("cmd", ".bat", &["/C"], self.cmd.as_ref()),
69 ("powershell", ".ps1", &["-File"], self.powershell.as_ref()),
70 ("node", ".js", &[], self.node.as_ref()),
71 ];
72
73 for &(command, ext, arg_prefix, sa_opt) in interpreters {
74 let Some(sa) = sa_opt else { continue };
75
76 let safe_name: String = self
77 .name
78 .chars()
79 .map(|c| if c.is_alphanumeric() { c } else { '_' })
80 .take(200)
81 .collect();
82 let safe_name = if safe_name.starts_with(|c: char| c.is_ascii_digit()) {
83 format!("_{safe_name}")
84 } else {
85 safe_name
86 };
87 let embedded_name = format!("{safe_name}_script");
88 let filename = format!("{embedded_name}{ext}");
89 let file_ref = format!("{{{{Task.File.{embedded_name}}}}}");
90
91 let mut args = Vec::new();
92 for prefix_arg in arg_prefix {
93 args.push(FormatString::new(prefix_arg).unwrap());
94 }
95 args.push(FormatString::new(&file_ref).unwrap());
96 if let Some(user_args) = &sa.args {
97 args.extend(user_args.iter().cloned());
98 }
99
100 return Ok(Some(StepScript {
101 let_bindings: sa.let_bindings.clone(),
102 actions: StepActions {
103 on_run: Action {
104 command: FormatString::new(command).unwrap(),
105 args: Some(args),
106 cancelation: sa.cancelation.clone(),
107 timeout: sa.timeout.clone(),
108 },
109 },
110 embedded_files: Some(vec![EmbeddedFile {
111 name: embedded_name,
112 file_type: crate::types::FileType::Text,
113 filename: Some(FormatString::new(&filename).unwrap()),
114 data: Some(FormatString::new(&sa.script).map_err(|e| {
115 crate::ModelError::DecodeValidation(format!(
116 "SimpleAction script format string error: {e}"
117 ))
118 })?),
119 runnable: Some(true),
120 end_of_line: None,
121 }]),
122 }));
123 }
124
125 Ok(None)
126 }
127}
128
129#[derive(Debug, Clone, Deserialize)]
131#[serde(rename_all = "camelCase", deny_unknown_fields)]
132pub struct StepDependency {
133 pub depends_on: String,
134}
135
136#[derive(Debug, Clone, Deserialize)]
138#[serde(rename_all = "camelCase", deny_unknown_fields)]
139pub struct StepScript {
140 #[serde(rename = "let")]
141 pub let_bindings: Option<Vec<String>>,
142 pub actions: StepActions,
143 pub embedded_files: Option<Vec<EmbeddedFile>>,
144}
145
146#[cfg(test)]
147mod tests {
148 use super::StepTemplate;
149
150 #[test]
151 fn resolve_syntax_sugar_returns_error_for_malformed_format_string() {
152 let step: StepTemplate = serde_saphyr::from_str(
153 r#"
154 name: TestStep
155 bash:
156 script: "echo '{{broken'"
157 "#,
158 )
159 .unwrap();
160
161 let result = step.resolve_syntax_sugar();
162 assert!(
163 result.is_err(),
164 "resolve_syntax_sugar should return Err for malformed format string"
165 );
166 }
167
168 #[test]
169 fn resolve_syntax_sugar_ok_for_valid_script() {
170 let step: StepTemplate = serde_saphyr::from_str(
171 r#"
172 name: TestStep
173 bash:
174 script: "echo hello"
175 "#,
176 )
177 .unwrap();
178
179 let result = step.resolve_syntax_sugar();
180 assert!(result.is_ok(), "valid script should succeed");
181 assert!(
182 result.unwrap().is_some(),
183 "bash step should produce a StepScript"
184 );
185 }
186}