1pub fn parse_sequence(value: &str) -> Vec<Option<String>> {
26 let parts: Vec<Option<String>> = value
28 .split_whitespace()
29 .map(|v| if v == "_" { None } else { Some(v.to_string()) })
30 .collect();
31 parts
32}
33
34pub fn format_sequence(sequence: &[Option<String>]) -> String {
47 if sequence.is_empty() {
48 return String::new();
49 }
50 sequence
51 .iter()
52 .map(|v| v.as_deref().unwrap_or("_"))
53 .collect::<Vec<_>>()
54 .join(" ")
55}
56
57pub fn shift_sequence(sequence: &[Option<String>]) -> Vec<Option<String>> {
67 if sequence.is_empty() {
68 return vec![];
69 }
70 sequence[1..].to_vec()
71}
72
73pub fn is_sequence(value: &str) -> bool {
84 value.contains(' ')
85}
86
87pub fn distribute_option(
110 option_value: &str,
111 stack_depth: usize,
112 option_name: &str,
113) -> Result<Vec<Option<String>>, String> {
114 if option_value.is_empty() {
115 return Ok(vec![None; stack_depth]);
116 }
117
118 let parsed = parse_sequence(option_value);
119
120 if parsed.len() == 1 && stack_depth > 1 {
122 return Ok(vec![parsed[0].clone(); stack_depth]);
123 }
124
125 if parsed.len() != stack_depth {
127 return Err(format!(
128 "{} has {} value(s) but isolation stack has {} level(s). \
129 Use underscores (_) as placeholders for levels that don't need this option.",
130 option_name,
131 parsed.len(),
132 stack_depth
133 ));
134 }
135
136 Ok(parsed)
137}
138
139pub fn get_value_at_level(distributed: &[Option<String>], level: usize) -> Option<&str> {
143 distributed.get(level)?.as_deref()
144}
145
146pub fn format_isolation_chain(stack: &[Option<String>], options: &IsolationChainOptions) -> String {
159 if stack.is_empty() {
160 return String::new();
161 }
162 stack
163 .iter()
164 .enumerate()
165 .map(|(i, backend)| match backend.as_deref() {
166 None => "_".to_string(),
167 Some("ssh") => {
168 if let Some(ep) = get_value_at_level(&options.endpoint_stack, i) {
169 format!("ssh@{}", ep)
170 } else {
171 "ssh".to_string()
172 }
173 }
174 Some("docker") => {
175 if let Some(image) = get_value_at_level(&options.image_stack, i) {
176 let short_name = image.split(':').next().unwrap_or(image);
177 let short_name = short_name.rsplit('/').next().unwrap_or(short_name);
178 format!("docker:{}", short_name)
179 } else {
180 "docker".to_string()
181 }
182 }
183 Some(b) => b.to_string(),
184 })
185 .collect::<Vec<_>>()
186 .join(" \u{2192} ")
187}
188
189#[derive(Default)]
191pub struct IsolationChainOptions {
192 pub endpoint_stack: Vec<Option<String>>,
194 pub image_stack: Vec<Option<String>>,
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn test_parse_sequence_single() {
204 assert_eq!(parse_sequence("docker"), vec![Some("docker".to_string())]);
205 }
206
207 #[test]
208 fn test_parse_sequence_multiple() {
209 assert_eq!(
210 parse_sequence("screen ssh docker"),
211 vec![
212 Some("screen".to_string()),
213 Some("ssh".to_string()),
214 Some("docker".to_string()),
215 ]
216 );
217 }
218
219 #[test]
220 fn test_parse_sequence_with_underscores() {
221 assert_eq!(
222 parse_sequence("_ ssh _ docker"),
223 vec![
224 None,
225 Some("ssh".to_string()),
226 None,
227 Some("docker".to_string()),
228 ]
229 );
230 }
231
232 #[test]
233 fn test_parse_sequence_all_underscores() {
234 assert_eq!(parse_sequence("_ _ _"), vec![None, None, None]);
235 }
236
237 #[test]
238 fn test_parse_sequence_empty_string() {
239 assert_eq!(parse_sequence(""), vec![]);
240 }
241
242 #[test]
243 fn test_parse_sequence_trims_whitespace() {
244 assert_eq!(
245 parse_sequence(" docker "),
246 vec![Some("docker".to_string())]
247 );
248 }
249
250 #[test]
251 fn test_parse_sequence_multiple_spaces() {
252 assert_eq!(
253 parse_sequence("screen ssh"),
254 vec![Some("screen".to_string()), Some("ssh".to_string())]
255 );
256 }
257
258 #[test]
259 fn test_format_sequence_with_values() {
260 let seq = vec![
261 Some("screen".to_string()),
262 Some("ssh".to_string()),
263 Some("docker".to_string()),
264 ];
265 assert_eq!(format_sequence(&seq), "screen ssh docker");
266 }
267
268 #[test]
269 fn test_format_sequence_with_nulls() {
270 let seq = vec![None, Some("ssh".to_string()), None];
271 assert_eq!(format_sequence(&seq), "_ ssh _");
272 }
273
274 #[test]
275 fn test_format_sequence_empty() {
276 assert_eq!(format_sequence(&[]), "");
277 }
278
279 #[test]
280 fn test_shift_sequence_removes_first() {
281 let seq = vec![
282 Some("screen".to_string()),
283 Some("ssh".to_string()),
284 Some("docker".to_string()),
285 ];
286 assert_eq!(
287 shift_sequence(&seq),
288 vec![Some("ssh".to_string()), Some("docker".to_string())]
289 );
290 }
291
292 #[test]
293 fn test_shift_sequence_with_nulls() {
294 let seq = vec![None, Some("ssh".to_string())];
295 assert_eq!(shift_sequence(&seq), vec![Some("ssh".to_string())]);
296 }
297
298 #[test]
299 fn test_shift_sequence_single_element() {
300 let seq = vec![Some("docker".to_string())];
301 assert_eq!(shift_sequence(&seq), vec![]);
302 }
303
304 #[test]
305 fn test_shift_sequence_empty() {
306 assert_eq!(shift_sequence(&[]), vec![]);
307 }
308
309 #[test]
310 fn test_is_sequence_true_for_space_separated() {
311 assert!(is_sequence("screen ssh docker"));
312 assert!(is_sequence("_ ssh _"));
313 }
314
315 #[test]
316 fn test_is_sequence_false_for_single_value() {
317 assert!(!is_sequence("docker"));
318 assert!(!is_sequence("screen"));
319 }
320
321 #[test]
322 fn test_is_sequence_false_for_empty() {
323 assert!(!is_sequence(""));
324 }
325
326 #[test]
327 fn test_distribute_option_single_value_replicates() {
328 let result = distribute_option("node:20", 3, "image").unwrap();
329 assert_eq!(
330 result,
331 vec![
332 Some("node:20".to_string()),
333 Some("node:20".to_string()),
334 Some("node:20".to_string()),
335 ]
336 );
337 }
338
339 #[test]
340 fn test_distribute_option_sequence_with_matching_length() {
341 let result = distribute_option("_ user@host _", 3, "endpoint").unwrap();
342 assert_eq!(result, vec![None, Some("user@host".to_string()), None]);
343 }
344
345 #[test]
346 fn test_distribute_option_throws_on_length_mismatch() {
347 let result = distribute_option("_ user@host", 3, "endpoint");
348 assert!(result.is_err());
349 assert!(result.unwrap_err().contains("endpoint"));
350 }
351
352 #[test]
353 fn test_distribute_option_empty_returns_nulls() {
354 let result = distribute_option("", 3, "endpoint").unwrap();
355 assert_eq!(result, vec![None, None, None]);
356 }
357
358 #[test]
359 fn test_get_value_at_level_valid_index() {
360 let dist = vec![Some("screen".to_string()), None, Some("docker".to_string())];
361 assert_eq!(get_value_at_level(&dist, 0), Some("screen"));
362 assert_eq!(get_value_at_level(&dist, 2), Some("docker"));
363 }
364
365 #[test]
366 fn test_get_value_at_level_null_value() {
367 let dist = vec![None, Some("ssh".to_string())];
368 assert_eq!(get_value_at_level(&dist, 0), None);
369 }
370
371 #[test]
372 fn test_get_value_at_level_out_of_bounds() {
373 let dist = vec![Some("docker".to_string())];
374 assert_eq!(get_value_at_level(&dist, 5), None);
375 }
376
377 #[test]
378 fn test_format_isolation_chain_simple() {
379 let stack = vec![Some("screen".to_string()), Some("docker".to_string())];
380 let opts = IsolationChainOptions::default();
381 assert_eq!(
382 format_isolation_chain(&stack, &opts),
383 "screen \u{2192} docker"
384 );
385 }
386
387 #[test]
388 fn test_format_isolation_chain_with_ssh_endpoint() {
389 let stack = vec![Some("screen".to_string()), Some("ssh".to_string())];
390 let opts = IsolationChainOptions {
391 endpoint_stack: vec![None, Some("user@host.com".to_string())],
392 image_stack: vec![],
393 };
394 assert_eq!(
395 format_isolation_chain(&stack, &opts),
396 "screen \u{2192} ssh@user@host.com"
397 );
398 }
399
400 #[test]
401 fn test_format_isolation_chain_with_docker_image_short_name() {
402 let stack = vec![Some("docker".to_string())];
403 let opts = IsolationChainOptions {
404 image_stack: vec![Some("myregistry.io/team/ubuntu:22.04".to_string())],
405 endpoint_stack: vec![],
406 };
407 assert_eq!(format_isolation_chain(&stack, &opts), "docker:ubuntu");
408 }
409
410 #[test]
411 fn test_format_isolation_chain_with_placeholders() {
412 let stack = vec![None, Some("ssh".to_string()), None];
413 let opts = IsolationChainOptions::default();
414 assert_eq!(
415 format_isolation_chain(&stack, &opts),
416 "_ \u{2192} ssh \u{2192} _"
417 );
418 }
419
420 #[test]
421 fn test_format_isolation_chain_empty() {
422 let opts = IsolationChainOptions::default();
423 assert_eq!(format_isolation_chain(&[], &opts), "");
424 }
425}