1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
use regex::{escape, Regex};

use crate::helpers::{read_file, Result};

/// Check that `path` contain the regular expression given by
/// `template`.
///
/// The placeholders `{name}` and `{version}` will be replaced with
/// `pkg_name` and `pkg_version`, if they are present in `template`.
/// It is okay if `template` do not contain these placeholders.
///
/// The matching is done in multi-line mode, which means that `^` in
/// the regular expression will match the beginning of any line in the
/// file, not just the very beginning of the file.
///
/// # Errors
///
/// If the regular expression cannot be found, an `Err` is returned
/// with a succinct error message. Status information has then already
/// been printed on `stdout`.
pub fn check_contains_regex(
    path: &str,
    template: &str,
    pkg_name: &str,
    pkg_version: &str,
) -> Result<()> {
    // Expand the optional {name} and {version} placeholders in the
    // template. This is almost like
    //
    //   format!(template, name = pkg_name, version = pkg_version)
    //
    // but allows the user to leave out unnecessary placeholders.
    let orig_regex = template
        .replace("{name}", &escape(pkg_name))
        .replace("{version}", &escape(pkg_version));

    // We start by constructing a Regex from the original string. This
    // ensurs that any errors refer to the string the user passed
    // instead of the string we use internally.
    let re = match Regex::new(&orig_regex) {
        Ok(_) => {
            // We now know that the regex is valid, so we can enable
            // multi-line mode by prepending "(?m)".
            let regex = String::from("(?m)") + &orig_regex;
            Regex::new(&regex).unwrap()
        }
        Err(err) => return Err(format!("could not parse template: {}", err)),
    };
    let text = read_file(path).map_err(|err| format!("could not read {}: {}", path, err))?;

    println!("Searching for \"{}\" in {}...", orig_regex, path);
    match re.find(&text) {
        Some(m) => {
            let line_no = text[..m.start()].lines().count();
            println!("{} (line {}) ... ok", path, line_no + 1);
            Ok(())
        }
        None => Err(format!("could not find \"{}\" in {}", orig_regex, path)),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn bad_regex() {
        // Check that the error from a bad pattern doesn't contain
        // the (?m) prefix.
        assert_eq!(
            check_contains_regex("README.md", "Version {version} [ups", "foobar", "1.2.3"),
            Err(String::from(
                [
                    r"could not parse template: regex parse error:",
                    r"    Version 1\.2\.3 [ups",
                    r"                    ^",
                    r"error: unclosed character class"
                ]
                .join("\n")
            ))
        )
    }

    #[test]
    fn not_found() {
        assert_eq!(
            check_contains_regex("README.md", "should not be found", "foobar", "1.2.3"),
            Err(String::from(
                "could not find \"should not be found\" in README.md"
            ))
        )
    }

    #[test]
    fn escaping() {
        assert_eq!(
            check_contains_regex(
                "README.md",
                "escaped: {name}-{version}, not escaped: foo*bar-1.2.3",
                "foo*bar",
                "1.2.3"
            ),
            Err(String::from(
                [
                    r#"could not find "escaped: foo\*bar-1\.2\.3,"#,
                    r#"not escaped: foo*bar-1.2.3" in README.md"#
                ]
                .join(" ")
            ))
        )
    }

    #[test]
    fn good_pattern() {
        assert_eq!(
            check_contains_regex("README.md", "{name}", "version-sync", "1.2.3"),
            Ok(())
        )
    }

}