blob: d3dcbc1cae27842263fb6c41c6c8640bf4cbcbdc [file] [log] [blame] [edit]
use std::borrow::Cow;
pub(crate) trait Substitute {
fn substitute<'v>(&self, value: &'v str) -> Cow<'v, str>;
fn clear<'v>(&self, value: &'v str) -> Cow<'v, str>;
}
pub(crate) struct NoOp;
impl Substitute for NoOp {
fn substitute<'v>(&self, value: &'v str) -> Cow<'v, str> {
Cow::Borrowed(value)
}
fn clear<'v>(&self, value: &'v str) -> Cow<'v, str> {
Cow::Borrowed(value)
}
}
#[derive(Default, Clone, Debug, PartialEq, Eq)]
pub(crate) struct Substitutions {
vars: std::collections::BTreeMap<&'static str, Cow<'static, str>>,
unused: std::collections::BTreeSet<&'static str>,
}
impl Substitutions {
pub(crate) fn insert(
&mut self,
key: &'static str,
value: impl Into<Cow<'static, str>>,
) -> Result<(), crate::Error> {
let key = validate_key(key)?;
let value = value.into();
if value.is_empty() {
self.unused.insert(key);
} else {
self.vars.insert(
key,
crate::filesystem::normalize_text(value.as_ref()).into(),
);
}
Ok(())
}
pub(crate) fn extend(
&mut self,
vars: impl IntoIterator<Item = (&'static str, impl Into<Cow<'static, str>>)>,
) -> Result<(), crate::Error> {
for (key, value) in vars {
self.insert(key, value)?;
}
Ok(())
}
}
impl Substitute for Substitutions {
fn substitute<'v>(&self, value: &'v str) -> Cow<'v, str> {
let mut value = Cow::Borrowed(value);
for (var, replace) in self.vars.iter() {
debug_assert!(!replace.is_empty());
value = Cow::Owned(value.replace(replace.as_ref(), var));
}
value
}
fn clear<'v>(&self, pattern: &'v str) -> Cow<'v, str> {
if pattern.contains('[') {
let mut pattern = Cow::Borrowed(pattern);
for var in self.unused.iter() {
pattern = Cow::Owned(pattern.replace(var, ""));
}
pattern
} else {
Cow::Borrowed(pattern)
}
}
}
fn validate_key(key: &'static str) -> Result<&'static str, crate::Error> {
if !key.starts_with('[') || !key.ends_with(']') {
return Err(format!("Key `{}` is not enclosed in []", key).into());
}
if key[1..(key.len() - 1)]
.find(|c: char| !c.is_ascii_uppercase())
.is_some()
{
return Err(format!("Key `{}` can only be A-Z but ", key).into());
}
Ok(key)
}
pub(crate) fn normalize(input: &str, pattern: &str, substitutions: &dyn Substitute) -> String {
if input == pattern {
return input.to_owned();
}
let mut normalized: Vec<Cow<str>> = Vec::new();
let input_lines: Vec<_> = crate::lines::LinesWithTerminator::new(input).collect();
let pattern_lines: Vec<_> = crate::lines::LinesWithTerminator::new(pattern).collect();
let mut input_index = 0;
let mut pattern_index = 0;
'outer: loop {
let pattern_line = if let Some(pattern_line) = pattern_lines.get(pattern_index) {
*pattern_line
} else {
normalized.extend(
input_lines[input_index..]
.iter()
.copied()
.map(|s| substitutions.substitute(s)),
);
break 'outer;
};
let next_pattern_index = pattern_index + 1;
let input_line = if let Some(input_line) = input_lines.get(input_index) {
*input_line
} else {
break 'outer;
};
let next_input_index = input_index + 1;
if line_matches(input_line, pattern_line, substitutions) {
pattern_index = next_pattern_index;
input_index = next_input_index;
normalized.push(Cow::Borrowed(pattern_line));
continue 'outer;
} else if is_line_elide(pattern_line) {
let next_pattern_line: &str =
if let Some(pattern_line) = pattern_lines.get(next_pattern_index) {
pattern_line
} else {
normalized.push(Cow::Borrowed(pattern_line));
break 'outer;
};
if let Some(future_input_index) = input_lines[input_index..]
.iter()
.enumerate()
.find(|(_, l)| **l == next_pattern_line)
.map(|(i, _)| input_index + i)
{
normalized.push(Cow::Borrowed(pattern_line));
pattern_index = next_pattern_index;
input_index = future_input_index;
continue 'outer;
} else {
normalized.extend(
input_lines[input_index..]
.iter()
.copied()
.map(|s| substitutions.substitute(s)),
);
break 'outer;
}
} else {
// Find where we can pick back up for normalizing
for future_input_index in next_input_index..input_lines.len() {
let future_input_line = input_lines[future_input_index];
if let Some(future_pattern_index) = pattern_lines[next_pattern_index..]
.iter()
.enumerate()
.find(|(_, l)| **l == future_input_line || is_line_elide(**l))
.map(|(i, _)| next_pattern_index + i)
{
normalized.extend(
input_lines[input_index..future_input_index]
.iter()
.copied()
.map(|s| substitutions.substitute(s)),
);
pattern_index = future_pattern_index;
input_index = future_input_index;
continue 'outer;
}
}
normalized.extend(
input_lines[input_index..]
.iter()
.copied()
.map(|s| substitutions.substitute(s)),
);
break 'outer;
}
}
normalized.join("")
}
fn is_line_elide(line: &str) -> bool {
line == "...\n" || line == "..."
}
fn line_matches(line: &str, pattern: &str, substitutions: &dyn Substitute) -> bool {
if line == pattern {
return true;
}
let subbed = substitutions.substitute(line);
let mut line = subbed.as_ref();
let pattern = substitutions.clear(pattern);
let mut sections = pattern.split("[..]").peekable();
while let Some(section) = sections.next() {
if let Some(remainder) = line.strip_prefix(section) {
if let Some(next_section) = sections.peek() {
if next_section.is_empty() {
line = "";
} else if let Some(restart_index) = remainder.find(next_section) {
line = &remainder[restart_index..];
}
} else {
return remainder.is_empty();
}
} else {
return false;
}
}
false
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn empty() {
let input = "";
let pattern = "";
let expected = "";
let actual = normalize(input, pattern, &NoOp);
assert_eq!(expected, actual);
}
#[test]
fn literals_match() {
let input = "Hello\nWorld";
let pattern = "Hello\nWorld";
let expected = "Hello\nWorld";
let actual = normalize(input, pattern, &NoOp);
assert_eq!(expected, actual);
}
#[test]
fn pattern_shorter() {
let input = "Hello\nWorld";
let pattern = "Hello\n";
let expected = "Hello\nWorld";
let actual = normalize(input, pattern, &NoOp);
assert_eq!(expected, actual);
}
#[test]
fn input_shorter() {
let input = "Hello\n";
let pattern = "Hello\nWorld";
let expected = "Hello\n";
let actual = normalize(input, pattern, &NoOp);
assert_eq!(expected, actual);
}
#[test]
fn all_different() {
let input = "Hello\nWorld";
let pattern = "Goodbye\nMoon";
let expected = "Hello\nWorld";
let actual = normalize(input, pattern, &NoOp);
assert_eq!(expected, actual);
}
#[test]
fn middles_diverge() {
let input = "Hello\nWorld\nGoodbye";
let pattern = "Hello\nMoon\nGoodbye";
let expected = "Hello\nWorld\nGoodbye";
let actual = normalize(input, pattern, &NoOp);
assert_eq!(expected, actual);
}
#[test]
fn leading_elide() {
let input = "Hello\nWorld\nGoodbye";
let pattern = "...\nGoodbye";
let expected = "...\nGoodbye";
let actual = normalize(input, pattern, &NoOp);
assert_eq!(expected, actual);
}
#[test]
fn trailing_elide() {
let input = "Hello\nWorld\nGoodbye";
let pattern = "Hello\n...";
let expected = "Hello\n...";
let actual = normalize(input, pattern, &NoOp);
assert_eq!(expected, actual);
}
#[test]
fn middle_elide() {
let input = "Hello\nWorld\nGoodbye";
let pattern = "Hello\n...\nGoodbye";
let expected = "Hello\n...\nGoodbye";
let actual = normalize(input, pattern, &NoOp);
assert_eq!(expected, actual);
}
#[test]
fn post_elide_diverge() {
let input = "Hello\nSun\nAnd\nWorld";
let pattern = "Hello\n...\nMoon";
let expected = "Hello\nSun\nAnd\nWorld";
let actual = normalize(input, pattern, &NoOp);
assert_eq!(expected, actual);
}
#[test]
fn post_diverge_elide() {
let input = "Hello\nWorld\nGoodbye\nSir";
let pattern = "Hello\nMoon\nGoodbye\n...";
let expected = "Hello\nWorld\nGoodbye\n...";
let actual = normalize(input, pattern, &NoOp);
assert_eq!(expected, actual);
}
#[test]
fn inline_elide() {
let input = "Hello\nWorld\nGoodbye\nSir";
let pattern = "Hello\nW[..]d\nGoodbye\nSir";
let expected = "Hello\nW[..]d\nGoodbye\nSir";
let actual = normalize(input, pattern, &NoOp);
assert_eq!(expected, actual);
}
#[test]
fn line_matches_cases() {
let cases = [
("", "", true),
("", "[..]", true),
("hello", "hello", true),
("hello", "goodbye", false),
("hello", "[..]", true),
("hello", "he[..]", true),
("hello", "go[..]", false),
("hello", "[..]o", true),
("hello", "[..]e", false),
("hello", "he[..]o", true),
("hello", "he[..]e", false),
("hello", "go[..]o", false),
("hello", "go[..]e", false),
(
"hello world, goodbye moon",
"hello [..], goodbye [..]",
true,
),
(
"hello world, goodbye moon",
"goodbye [..], goodbye [..]",
false,
),
(
"hello world, goodbye moon",
"goodbye [..], hello [..]",
false,
),
("hello world, goodbye moon", "hello [..], [..] moon", true),
(
"hello world, goodbye moon",
"goodbye [..], [..] moon",
false,
),
("hello world, goodbye moon", "hello [..], [..] world", false),
];
for (line, pattern, expected) in cases {
let actual = line_matches(line, pattern, &NoOp);
assert_eq!(actual, expected, "line={:?} pattern={:?}", line, pattern);
}
}
#[test]
fn test_validate_key() {
let cases = [
("[HELLO", false),
("HELLO]", false),
("[HELLO]", true),
("[hello]", false),
("[HE O]", false),
];
for (key, expected) in cases {
let actual = validate_key(key).is_ok();
assert_eq!(actual, expected, "key={:?}", key);
}
}
}