1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub enum PipelineExpr {
13 Single(String),
15 Pipe(Box<PipelineExpr>, Box<PipelineExpr>),
17 Sequential(Box<PipelineExpr>, Box<PipelineExpr>),
19 Parallel(Vec<PipelineExpr>),
21}
22
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25#[serde(rename_all = "lowercase")]
26pub enum PipelineStatus {
27 Success,
28 Failed,
29 Skipped,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct PipelineOutput {
35 pub workflow_id: String,
36 pub status: PipelineStatus,
37 pub output_text: Option<String>,
38 pub output_data: Option<serde_json::Value>,
39 pub exit_code: i32,
40 pub duration_ms: u64,
41}
42
43pub fn inject_input(template: &str, input: Option<&str>) -> String {
49 let replacement = input.unwrap_or("");
50 let escaped = shell_escape::escape(replacement.into());
51 template.replace("{{input}}", &escaped)
52}
53
54pub fn has_pipeline_syntax(input: &str) -> bool {
56 input.contains(" | ") || input.contains(" && ") || input.contains(", ") || input.contains(',')
58}
59
60pub fn parse_pipeline_expr(input: &str) -> Result<PipelineExpr, PipelineParseError> {
67 let input = input.trim();
68 if input.is_empty() {
69 return Err(PipelineParseError::EmptyInput);
70 }
71 parse_parallel(input)
72}
73
74#[derive(Debug, Clone, PartialEq, thiserror::Error)]
76pub enum PipelineParseError {
77 #[error("empty pipeline expression")]
78 EmptyInput,
79 #[error("empty segment in pipeline expression")]
80 EmptySegment,
81}
82
83fn parse_parallel(input: &str) -> Result<PipelineExpr, PipelineParseError> {
85 let parts: Vec<&str> = input.split(',').collect();
86 if parts.len() == 1 {
87 return parse_sequential(input);
88 }
89
90 let mut exprs = Vec::with_capacity(parts.len());
91 for part in parts {
92 let part = part.trim();
93 if part.is_empty() {
94 return Err(PipelineParseError::EmptySegment);
95 }
96 exprs.push(parse_sequential(part)?);
97 }
98
99 if exprs.len() == 1 {
100 Ok(exprs.into_iter().next().unwrap())
101 } else {
102 Ok(PipelineExpr::Parallel(exprs))
103 }
104}
105
106fn parse_sequential(input: &str) -> Result<PipelineExpr, PipelineParseError> {
108 let parts: Vec<&str> = input.split("&&").collect();
109 if parts.len() == 1 {
110 return parse_pipe(input);
111 }
112
113 let mut iter = parts.into_iter();
114 let first = iter.next().unwrap().trim();
115 if first.is_empty() {
116 return Err(PipelineParseError::EmptySegment);
117 }
118 let mut expr = parse_pipe(first)?;
119
120 for part in iter {
121 let part = part.trim();
122 if part.is_empty() {
123 return Err(PipelineParseError::EmptySegment);
124 }
125 let right = parse_pipe(part)?;
126 expr = PipelineExpr::Sequential(Box::new(expr), Box::new(right));
127 }
128
129 Ok(expr)
130}
131
132fn parse_pipe(input: &str) -> Result<PipelineExpr, PipelineParseError> {
134 let parts: Vec<&str> = input.split('|').collect();
135 if parts.len() == 1 {
136 return parse_single(input);
137 }
138
139 let mut iter = parts.into_iter();
140 let first = iter.next().unwrap().trim();
141 if first.is_empty() {
142 return Err(PipelineParseError::EmptySegment);
143 }
144 let mut expr = parse_single(first)?;
145
146 for part in iter {
147 let part = part.trim();
148 if part.is_empty() {
149 return Err(PipelineParseError::EmptySegment);
150 }
151 let right = parse_single(part)?;
152 expr = PipelineExpr::Pipe(Box::new(expr), Box::new(right));
153 }
154
155 Ok(expr)
156}
157
158fn parse_single(input: &str) -> Result<PipelineExpr, PipelineParseError> {
160 let input = input.trim();
161 if input.is_empty() {
162 return Err(PipelineParseError::EmptySegment);
163 }
164 Ok(PipelineExpr::Single(input.to_string()))
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_single() {
173 assert_eq!(
174 parse_pipeline_expr("w1").unwrap(),
175 PipelineExpr::Single("w1".into())
176 );
177 }
178
179 #[test]
180 fn test_pipe() {
181 assert_eq!(
182 parse_pipeline_expr("w1 | w2").unwrap(),
183 PipelineExpr::Pipe(
184 Box::new(PipelineExpr::Single("w1".into())),
185 Box::new(PipelineExpr::Single("w2".into())),
186 )
187 );
188 }
189
190 #[test]
191 fn test_sequential() {
192 assert_eq!(
193 parse_pipeline_expr("w1 && w2").unwrap(),
194 PipelineExpr::Sequential(
195 Box::new(PipelineExpr::Single("w1".into())),
196 Box::new(PipelineExpr::Single("w2".into())),
197 )
198 );
199 }
200
201 #[test]
202 fn test_parallel() {
203 assert_eq!(
204 parse_pipeline_expr("w1, w2").unwrap(),
205 PipelineExpr::Parallel(vec![
206 PipelineExpr::Single("w1".into()),
207 PipelineExpr::Single("w2".into()),
208 ])
209 );
210 }
211
212 #[test]
213 fn test_pipe_then_sequential() {
214 assert_eq!(
216 parse_pipeline_expr("w1 | w2 && w3").unwrap(),
217 PipelineExpr::Sequential(
218 Box::new(PipelineExpr::Pipe(
219 Box::new(PipelineExpr::Single("w1".into())),
220 Box::new(PipelineExpr::Single("w2".into())),
221 )),
222 Box::new(PipelineExpr::Single("w3".into())),
223 )
224 );
225 }
226
227 #[test]
228 fn test_full_combo() {
229 assert_eq!(
235 parse_pipeline_expr("w1 | w2 && w3, w4").unwrap(),
236 PipelineExpr::Parallel(vec![
237 PipelineExpr::Sequential(
238 Box::new(PipelineExpr::Pipe(
239 Box::new(PipelineExpr::Single("w1".into())),
240 Box::new(PipelineExpr::Single("w2".into())),
241 )),
242 Box::new(PipelineExpr::Single("w3".into())),
243 ),
244 PipelineExpr::Single("w4".into()),
245 ])
246 );
247 }
248
249 #[test]
250 fn test_empty_input() {
251 assert_eq!(parse_pipeline_expr(""), Err(PipelineParseError::EmptyInput));
252 }
253
254 #[test]
255 fn test_triple_pipe() {
256 assert_eq!(
258 parse_pipeline_expr("w1 | w2 | w3").unwrap(),
259 PipelineExpr::Pipe(
260 Box::new(PipelineExpr::Pipe(
261 Box::new(PipelineExpr::Single("w1".into())),
262 Box::new(PipelineExpr::Single("w2".into())),
263 )),
264 Box::new(PipelineExpr::Single("w3".into())),
265 )
266 );
267 }
268
269 #[test]
270 fn test_has_pipeline_syntax() {
271 assert!(!has_pipeline_syntax("simple-workflow"));
272 assert!(has_pipeline_syntax("w1 | w2"));
273 assert!(has_pipeline_syntax("w1 && w2"));
274 assert!(has_pipeline_syntax("w1, w2"));
275 }
276
277 #[test]
278 fn test_inject_input_with_value() {
279 let result = inject_input("Analyze this: {{input}}\nDone.", Some("hello world"));
281 assert!(
283 result == "Analyze this: 'hello world'\nDone."
284 || result == "Analyze this: \"hello world\"\nDone."
285 );
286 }
287
288 #[test]
289 fn test_inject_input_none() {
290 let result = inject_input("Prefix {{input}} suffix", None);
291 assert!(result == "Prefix '' suffix" || result == "Prefix \"\" suffix");
292 }
293
294 #[test]
295 fn test_inject_input_no_placeholder() {
296 let result = inject_input("no placeholder here", Some("data"));
297 assert_eq!(result, "no placeholder here");
298 }
299
300 #[test]
301 fn test_inject_input_multiple() {
302 let result = inject_input("{{input}} and {{input}}", Some("x"));
303 assert_eq!(result, "x and x");
304 }
305
306 #[test]
307 fn test_inject_input_shell_injection_prevented() {
308 let result = inject_input("echo {{input}}", Some("hello; rm -rf /"));
309 assert!(result.contains("'hello; rm -rf /'") || result.contains("\"hello; rm -rf /\""));
312 }
313}