coliru

A minimal, flexible, dotfile installer
git clone https://git.ashermorgan.net/coliru/
Log | Files | Refs | README

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 }