manifest.rs (17239B)
1 //! Coliru manifest parsing and tag matching 2 3 use anyhow::Result; 4 use serde::Deserialize; 5 use serde_yaml; 6 use std::collections::HashSet; 7 use std::fs::read_to_string; 8 use std::path::{Path, PathBuf}; 9 10 /// The options for a copy or link command 11 #[derive(Clone, Debug, PartialEq, Deserialize)] 12 pub struct CopyLinkOptions { 13 /// The source file (relative to the parent manifest file) 14 pub src: String, 15 16 /// The destination path (relative to the parent manifest file) 17 pub dst: String, 18 } 19 20 /// The options for a run command 21 #[derive(Clone, Debug, PartialEq, Deserialize)] 22 pub struct RunOptions { 23 /// The location of the script (relative to the parent manifest file) 24 pub src: String, 25 26 /// The optional shell command prefix 27 #[serde(default)] 28 pub prefix: String, 29 30 /// The optional shell command postfix 31 #[serde(default)] 32 pub postfix: String, 33 } 34 35 /// A manifest step 36 #[derive(Clone, Debug, PartialEq, Deserialize)] 37 pub struct Step { 38 /// The step's copy commands 39 #[serde(default)] 40 pub copy: Vec<CopyLinkOptions>, 41 42 /// The step's link commands 43 #[serde(default)] 44 pub link: Vec<CopyLinkOptions>, 45 46 /// The step's run commands 47 #[serde(default)] 48 pub run: Vec<RunOptions>, 49 50 /// The step's tags 51 #[serde(default)] 52 pub tags: Vec<String>, 53 } 54 55 /// A coliru manifest as it appears in a file, without the base_dir property 56 #[derive(Debug, PartialEq, Deserialize)] 57 struct RawManifest { 58 59 /// The manifest steps 60 steps: Vec<Step>, 61 } 62 63 /// A parsed coliru manifest 64 #[derive(Clone, Debug, PartialEq)] 65 pub struct Manifest { 66 /// The manifest steps 67 pub steps: Vec<Step>, 68 69 /// The parent directory of the manifest file 70 pub base_dir: PathBuf, 71 } 72 73 /// Checks if a list of tags matches a list of tag rules 74 /// 75 /// ``` 76 /// let rules = ["linux,macos", "system", "^work"]; 77 /// let tags_1 = ["macos", "system", "user"]; 78 /// let tags_2 = ["linux", "system", "work"]; 79 /// assert_eq!(tags_match(&rules, &tags_1), true); 80 /// assert_eq!(tags_match(&rules, &tags_2), false); 81 /// ``` 82 fn tags_match<S: AsRef<str>>(rules: &[S], tags: &[S]) -> bool { 83 for rule in rules.iter() { 84 let mut _rule = rule.as_ref(); 85 let is_negated = _rule.chars().nth(0) == Some('^'); 86 if is_negated { 87 _rule = &_rule[1..]; // Strip leading '^' 88 } 89 90 let tag_found = _rule.split(",").any(|subrule| { 91 tags.iter().any(|tag| { 92 tag.as_ref() == subrule 93 }) 94 }); 95 96 if tag_found == is_negated { 97 return false 98 } 99 } 100 101 true 102 } 103 104 /// Parse a coliru YAML manifest file 105 /// 106 /// ``` 107 /// let manifest = parse_manifest_file(Path::new("manifest.yml"))?; 108 /// ``` 109 pub fn parse_manifest_file(path: &Path) -> Result<Manifest> { 110 let raw_str = read_to_string(path)?; 111 let raw_manifest = serde_yaml::from_str::<RawManifest>(&raw_str)?; 112 let base_dir = match path.parent() { 113 None => &Path::new("."), 114 Some(p) => if p == Path::new("") { &Path::new(".") } else { p }, 115 }; 116 117 Ok(Manifest { 118 steps: raw_manifest.steps, 119 base_dir: base_dir.to_path_buf(), 120 }) 121 } 122 123 /// Returns a sorted, de-duplicated vector of all tags in a manifest 124 /// 125 /// ``` 126 /// let manifest = parse_manifest_file(Path::new("manifest.yml"))?; 127 /// let tags = get_manifest_tags(manifest); 128 /// ``` 129 pub fn get_manifest_tags(manifest: Manifest) -> Vec<String> { 130 let mut tag_set: HashSet<String> = HashSet::new(); 131 132 for step in manifest.steps { 133 for tag in step.tags { 134 tag_set.insert(tag); 135 } 136 } 137 138 let mut tags: Vec<String> = tag_set.iter().map(|s| s.to_owned()).collect(); 139 tags.sort(); 140 tags 141 } 142 143 /// Filter a manifest to only include steps that satisfy a set of tag rules 144 /// 145 /// ``` 146 /// let manifest = parse_manifest_file(Path::new("manifest.yml"))?; 147 /// let tag_rules = [String::from("linux"), String::from("^windows")]; 148 /// let filtered_manifest = filter_manifest_steps(manifest, &tag_rules); 149 /// let filtered_tags = get_manifest_tags(filtered_manifest); 150 /// assert_eq!(filtered_tags.contains(String::from("windows")), false); 151 /// ``` 152 pub fn filter_manifest_steps(manifest: Manifest, tag_rules: &[String]) -> 153 Manifest { 154 155 Manifest { 156 steps: manifest.steps.iter().filter(|x| 157 tags_match(tag_rules, &x.tags) 158 ).map(|x| x.clone()).collect(), 159 base_dir: manifest.base_dir, 160 } 161 } 162 163 #[cfg(test)] 164 mod tests { 165 use super::*; 166 167 #[test] 168 fn test_manifest_tags_match_empty_parameters() { 169 let tags_1 = []; 170 let tags_2 = ["linux", "user"]; 171 assert_eq!(tags_match(&tags_1, &tags_1), true); 172 assert_eq!(tags_match(&tags_1, &tags_2), true); 173 assert_eq!(tags_match(&tags_2, &tags_1), false); 174 } 175 176 #[test] 177 fn test_manifest_tags_match_one_match() { 178 let tags_1 = ["linux"]; 179 let tags_2 = ["linux", "windows"]; 180 181 assert_eq!(tags_match(&tags_1.clone(), &tags_1.clone()), true); 182 assert_eq!(tags_match(&tags_1.clone(), &tags_2.clone()), true); 183 assert_eq!(tags_match(&tags_2.clone(), &tags_1.clone()), false); 184 assert_eq!(tags_match(&tags_2.clone(), &tags_2.clone()), true); 185 } 186 187 #[test] 188 fn test_manifest_tags_match_two_matches() { 189 let tags_1 = ["linux", "user"]; 190 let tags_2 = ["linux", "user", "windows"]; 191 192 assert_eq!(tags_match(&tags_1.clone(), &tags_1.clone()), true); 193 assert_eq!(tags_match(&tags_1.clone(), &tags_2.clone()), true); 194 assert_eq!(tags_match(&tags_2.clone(), &tags_1.clone()), false); 195 assert_eq!(tags_match(&tags_2.clone(), &tags_2.clone()), true); 196 } 197 198 #[test] 199 fn test_manifest_tags_match_negated() { 200 let rules = ["^linux"]; 201 let tags_1 = ["linux"]; 202 let tags_2 = ["windows"]; 203 let tags_3 = ["macos"]; 204 let tags_4 = ["linux", "macos"]; 205 206 assert_eq!(tags_match(&rules.clone(), &tags_1.clone()), false); 207 assert_eq!(tags_match(&rules.clone(), &tags_2.clone()), true); 208 assert_eq!(tags_match(&rules.clone(), &tags_3.clone()), true); 209 assert_eq!(tags_match(&rules.clone(), &tags_4.clone()), false); 210 } 211 212 #[test] 213 fn test_manifest_tags_match_negated_two_rules() { 214 let rules_1 = ["^linux", "^user"]; 215 let rules_2 = ["^linux", "user"]; 216 let tags_1 = ["linux", "system"]; 217 let tags_2 = ["windows", "user"]; 218 let tags_3 = ["macos", "system"]; 219 let tags_4 = ["linux", "macos", "user"]; 220 221 assert_eq!(tags_match(&rules_1.clone(), &tags_1.clone()), false); 222 assert_eq!(tags_match(&rules_1.clone(), &tags_2.clone()), false); 223 assert_eq!(tags_match(&rules_1.clone(), &tags_3.clone()), true); 224 assert_eq!(tags_match(&rules_1.clone(), &tags_4.clone()), false); 225 assert_eq!(tags_match(&rules_2.clone(), &tags_1.clone()), false); 226 assert_eq!(tags_match(&rules_2.clone(), &tags_2.clone()), true); 227 assert_eq!(tags_match(&rules_2.clone(), &tags_3.clone()), false); 228 assert_eq!(tags_match(&rules_2.clone(), &tags_4.clone()), false); 229 } 230 231 #[test] 232 fn test_manifest_tags_match_union() { 233 let rules = ["linux,macos"]; 234 let tags_1 = ["linux"]; 235 let tags_2 = ["macos"]; 236 let tags_3 = ["linux", "macos"]; 237 let tags_4 = ["windows"]; 238 239 assert_eq!(tags_match(&rules.clone(), &tags_1.clone()), true); 240 assert_eq!(tags_match(&rules.clone(), &tags_2.clone()), true); 241 assert_eq!(tags_match(&rules.clone(), &tags_3.clone()), true); 242 assert_eq!(tags_match(&rules.clone(), &tags_4.clone()), false); 243 } 244 245 #[test] 246 fn test_manifest_tags_match_union_two_rules() { 247 let rules_1 = ["linux,macos", "user,system"]; 248 let rules_2 = ["linux,macos", "user"]; 249 let tags_1 = ["user", "linux"]; 250 let tags_2 = ["system", "macos"]; 251 let tags_3 = ["user", "linux", "macos"]; 252 let tags_4 = ["system", "windows"]; 253 254 assert_eq!(tags_match(&rules_1.clone(), &tags_1.clone()), true); 255 assert_eq!(tags_match(&rules_1.clone(), &tags_2.clone()), true); 256 assert_eq!(tags_match(&rules_1.clone(), &tags_3.clone()), true); 257 assert_eq!(tags_match(&rules_1.clone(), &tags_4.clone()), false); 258 assert_eq!(tags_match(&rules_2.clone(), &tags_1.clone()), true); 259 assert_eq!(tags_match(&rules_2.clone(), &tags_2.clone()), false); 260 assert_eq!(tags_match(&rules_2.clone(), &tags_3.clone()), true); 261 assert_eq!(tags_match(&rules_2.clone(), &tags_4.clone()), false); 262 } 263 264 #[test] 265 fn test_manifest_tags_match_union_negated() { 266 let rules = ["^linux,macos"]; 267 let tags_1 = ["linux"]; 268 let tags_2 = ["macos"]; 269 let tags_3 = ["linux", "macos"]; 270 let tags_4 = ["windows"]; 271 272 assert_eq!(tags_match(&rules.clone(), &tags_1.clone()), false); 273 assert_eq!(tags_match(&rules.clone(), &tags_2.clone()), false); 274 assert_eq!(tags_match(&rules.clone(), &tags_3.clone()), false); 275 assert_eq!(tags_match(&rules.clone(), &tags_4.clone()), true); 276 } 277 278 #[test] 279 fn test_manifest_tags_match_union_negated_two_rules() { 280 let rules_1 = ["^linux,macos", "^user"]; 281 let rules_2 = ["^linux,macos", "user,system"]; 282 let rules_3 = ["^linux,macos", "user"]; 283 let tags_1 = ["linux", "macos", "system"]; 284 let tags_2 = ["windows", "user"]; 285 let tags_3 = ["windows", "system"]; 286 287 assert_eq!(tags_match(&rules_1.clone(), &tags_1.clone()), false); 288 assert_eq!(tags_match(&rules_1.clone(), &tags_2.clone()), false); 289 assert_eq!(tags_match(&rules_1.clone(), &tags_3.clone()), true); 290 assert_eq!(tags_match(&rules_2.clone(), &tags_1.clone()), false); 291 assert_eq!(tags_match(&rules_2.clone(), &tags_2.clone()), true); 292 assert_eq!(tags_match(&rules_2.clone(), &tags_3.clone()), true); 293 assert_eq!(tags_match(&rules_3.clone(), &tags_1.clone()), false); 294 assert_eq!(tags_match(&rules_3.clone(), &tags_2.clone()), true); 295 assert_eq!(tags_match(&rules_3.clone(), &tags_3.clone()), false); 296 } 297 298 #[test] 299 #[cfg(target_family = "unix")] 300 fn test_manifest_parse_manifest_file_missing() { 301 let manifest_path = Path::new("examples/test/missing.yml"); 302 let expected = "No such file or directory (os error 2)"; 303 let actual = parse_manifest_file(manifest_path); 304 assert_eq!(actual.is_ok(), false); 305 assert_eq!(actual.unwrap_err().to_string(), expected); 306 } 307 308 #[test] 309 #[cfg(target_family = "windows")] 310 fn test_manifest_parse_manifest_file_missing() { 311 let manifest_path = Path::new("examples/test/missing.yml"); 312 let exp = "The system cannot find the file specified. (os error 2)"; 313 let actual = parse_manifest_file(manifest_path); 314 assert_eq!(actual.is_ok(), false); 315 assert_eq!(actual.unwrap_err().to_string(), exp); 316 } 317 318 #[test] 319 fn test_manifest_parse_manifest_file_invalid() { 320 let manifest_path = Path::new("examples/test/invalid.yml"); 321 let exp = "steps[0].copy[0]: missing field `src` at line 5 column 7"; 322 let actual = parse_manifest_file(manifest_path); 323 assert_eq!(actual.is_ok(), false); 324 assert_eq!(actual.unwrap_err().to_string(), exp); 325 } 326 327 #[test] 328 fn test_manifest_parse_manifest_file_valid() { 329 let manifest_path = Path::new("examples/test/manifest.yml"); 330 let expected = Manifest { 331 steps: vec![ 332 Step { 333 copy: vec![ 334 CopyLinkOptions { 335 src: String::from("gitconfig"), 336 dst: String::from("~/.gitconfig"), 337 }, 338 ], 339 link: vec![], 340 run: vec![], 341 tags: vec![ 342 String::from("windows"), 343 String::from("linux"), 344 String::from("macos") 345 ], 346 }, 347 Step { 348 copy: vec![ 349 CopyLinkOptions { 350 src: String::from("scripts/foo"), 351 dst: String::from("scripts/foo"), 352 }, 353 ], 354 link: vec![ 355 CopyLinkOptions { 356 src: String::from("bashrc"), 357 dst: String::from("~/.bashrc"), 358 }, 359 CopyLinkOptions { 360 src: String::from("vimrc"), 361 dst: String::from("~/.vimrc"), 362 }, 363 ], 364 run: vec![ 365 RunOptions { 366 src: String::from("scripts/script.sh"), 367 prefix: String::from("sh"), 368 postfix: String::from("arg1 $COLIRU_RULES"), 369 }, 370 ], 371 tags: vec![String::from("linux"), String::from("macos")], 372 }, 373 Step { 374 copy: vec![ 375 CopyLinkOptions { 376 src: String::from("scripts/foo"), 377 dst: String::from("scripts/foo"), 378 }, 379 ], 380 link: vec![ 381 CopyLinkOptions { 382 src: String::from("vimrc"), 383 dst: String::from("~/_vimrc"), 384 }, 385 ], 386 run: vec![ 387 RunOptions { 388 src: String::from("scripts/script.bat"), 389 prefix: String::from(""), 390 postfix: String::from("arg1 $COLIRU_RULES"), 391 }, 392 ], 393 tags: vec![String::from("windows")], 394 }, 395 ], 396 base_dir: PathBuf::from("examples/test"), 397 }; 398 let actual = parse_manifest_file(manifest_path); 399 assert_eq!(actual.is_ok(), true); 400 assert_eq!(actual.unwrap(), expected); 401 } 402 403 #[test] 404 fn test_manifest_get_manifest_tags_basic() { 405 let manifest_path = Path::new("examples/test/manifest.yml"); 406 let manifest = parse_manifest_file(manifest_path).unwrap(); 407 let expected = vec![ 408 String::from("linux"), 409 String::from("macos"), 410 String::from("windows"), 411 ]; 412 let actual = get_manifest_tags(manifest); 413 assert_eq!(actual, expected); 414 } 415 416 #[test] 417 fn test_manifest_get_manifest_tags_empty_manifest() { 418 let manifest = Manifest { 419 steps: vec![], 420 base_dir: PathBuf::from("examples/test/empty.yml"), 421 }; 422 let expected: Vec<String> = vec![]; 423 let actual = get_manifest_tags(manifest); 424 assert_eq!(actual, expected); 425 } 426 427 #[test] 428 fn test_manifest_get_manifest_tags_no_tags() { 429 let manifest_path = Path::new("examples/test/manifest.yml"); 430 let mut manifest = parse_manifest_file(manifest_path).unwrap(); 431 manifest.steps[0].tags = vec![]; 432 manifest.steps[1].tags = vec![]; 433 manifest.steps[2].tags = vec![]; 434 let expected: Vec<String> = vec![]; 435 let actual = get_manifest_tags(manifest); 436 assert_eq!(actual, expected); 437 } 438 439 #[test] 440 fn test_manifest_filter_manifest_steps_basic() { 441 let manifest_path = Path::new("examples/test/manifest.yml"); 442 let manifest = parse_manifest_file(manifest_path).unwrap(); 443 let tags = [String::from("linux")]; 444 let mut expected = manifest.clone(); 445 expected.steps.remove(2); 446 let actual = filter_manifest_steps(manifest, &tags); 447 assert_eq!(actual, expected); 448 } 449 450 #[test] 451 fn test_manifest_filter_manifest_steps_alternate_tags() { 452 let manifest_path = Path::new("examples/test/manifest.yml"); 453 let manifest = parse_manifest_file(manifest_path).unwrap(); 454 let tags = [String::from("linux"), String::from("^windows")]; 455 let mut expected = manifest.clone(); 456 expected.steps.remove(0); 457 expected.steps.remove(1); 458 let actual = filter_manifest_steps(manifest, &tags); 459 assert_eq!(actual, expected); 460 } 461 462 #[test] 463 fn test_manifest_filter_manifest_steps_empty_manifest() { 464 let manifest = Manifest { 465 steps: vec![], 466 base_dir: PathBuf::from("examples/test/empty.yml"), 467 }; 468 let tags = [String::from("linux")]; 469 let expected = manifest.clone(); 470 let actual = filter_manifest_steps(manifest, &tags); 471 assert_eq!(actual, expected); 472 } 473 474 #[test] 475 fn test_manifest_filter_manifest_steps_no_tags() { 476 let manifest_path = Path::new("examples/test/manifest.yml"); 477 let manifest = parse_manifest_file(manifest_path).unwrap(); 478 let tags = []; 479 let expected = manifest.clone(); 480 let actual = filter_manifest_steps(manifest, &tags); 481 assert_eq!(actual, expected); 482 } 483 }