use thiserror::Error;
use crate::UnservedInterface;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WitLevel {
pub level: u64,
pub packages: Vec<(String, String)>,
}
impl WitLevel {
#[must_use]
pub fn version_of(&self, package: &str) -> Option<&str> {
self.packages
.iter()
.find(|(name, _)| name == package)
.map(|(_, version)| version.as_str())
}
#[must_use]
pub fn served(&self) -> Vec<(&str, &str)> {
self.packages
.iter()
.map(|(package, version)| (package.as_str(), version.as_str()))
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum WitLevelsError {
#[error("line {line}: expected `<level> <package>@<version>`, found {fields} field(s)")]
FieldCount { line: usize, fields: usize },
#[error("line {line}: level '{text}' is not a positive integer")]
NotALevel { line: usize, text: String },
#[error("line {line}: the first level is {level}; levels are numbered from 1")]
FirstLevelNotOne { line: usize, level: u64 },
#[error(
"line {line}: level {level} follows level {previous}; levels are numbered with no gap"
)]
Gap {
line: usize,
level: u64,
previous: u64,
},
#[error(
"line {line}: level {level} follows level {previous}; a level's lines are contiguous and \
levels ascend"
)]
OutOfOrder {
line: usize,
level: u64,
previous: u64,
},
#[error("line {line}: '{text}' is not a murmur:<name>@<version> package")]
BadPackage { line: usize, text: String },
#[error("line {line}: version '{version}' of {package} is not <digits>.<digits>.<digits>")]
BadVersion {
line: usize,
package: String,
version: String,
},
#[error("line {line}: {package} is listed twice in level {level}")]
DuplicatePackage {
line: usize,
level: u64,
package: String,
},
#[error(
"line {line}: {package} follows {previous} in level {level}; a level lists its packages \
in ascending order"
)]
PackageOrder {
line: usize,
level: u64,
package: String,
previous: String,
},
#[error("the file lists no level; level 1 is the first")]
NoLevels,
#[error(
"line {line}: level {level} lists the same package set as level {previous}; append a \
level only when a version changes"
)]
SameAsPrevious {
line: usize,
level: u64,
previous: u64,
},
}
pub fn parse_wit_levels(text: &str) -> Result<Vec<WitLevel>, WitLevelsError> {
let mut levels: Vec<WitLevel> = Vec::new();
let mut first_lines: Vec<usize> = Vec::new();
for (index, raw) in text.lines().enumerate() {
let line = index + 1;
let content = raw.split_once('#').map_or(raw, |(before, _)| before);
let fields: Vec<&str> = content.split_whitespace().collect();
if fields.is_empty() {
continue;
}
if fields.len() != 2 {
return Err(WitLevelsError::FieldCount {
line,
fields: fields.len(),
});
}
let level = parse_level(fields[0]).ok_or_else(|| WitLevelsError::NotALevel {
line,
text: fields[0].to_string(),
})?;
let (package, version) = parse_package(line, fields[1])?;
let current = levels.last().map(|l| l.level);
match current {
None if level != 1 => return Err(WitLevelsError::FirstLevelNotOne { line, level }),
Some(previous) if level < previous => {
return Err(WitLevelsError::OutOfOrder {
line,
level,
previous,
})
}
Some(previous) if level > previous + 1 => {
return Err(WitLevelsError::Gap {
line,
level,
previous,
})
}
_ => {}
}
if current != Some(level) {
check_differs_from_previous(&levels, &first_lines)?;
levels.push(WitLevel {
level,
packages: Vec::new(),
});
first_lines.push(line);
}
let open = levels.last_mut().expect("a level was just opened");
if let Some((previous, _)) = open.packages.last() {
if *previous == package {
return Err(WitLevelsError::DuplicatePackage {
line,
level,
package,
});
}
if *previous > package {
return Err(WitLevelsError::PackageOrder {
line,
level,
package,
previous: previous.clone(),
});
}
}
open.packages.push((package, version));
}
if levels.is_empty() {
return Err(WitLevelsError::NoLevels);
}
check_differs_from_previous(&levels, &first_lines)?;
Ok(levels)
}
fn check_differs_from_previous(
levels: &[WitLevel],
first_lines: &[usize],
) -> Result<(), WitLevelsError> {
if let [.., previous, newest] = levels {
if previous.packages == newest.packages {
return Err(WitLevelsError::SameAsPrevious {
line: first_lines[first_lines.len() - 1],
level: newest.level,
previous: previous.level,
});
}
}
Ok(())
}
fn parse_level(text: &str) -> Option<u64> {
let level: u64 = text.parse().ok()?;
(level > 0 && level.to_string() == text).then_some(level)
}
fn parse_package(line: usize, text: &str) -> Result<(String, String), WitLevelsError> {
let bad_package = || WitLevelsError::BadPackage {
line,
text: text.to_string(),
};
let (package, version) = text.split_once('@').ok_or_else(bad_package)?;
let name = package.strip_prefix("murmur:").ok_or_else(bad_package)?;
if name.is_empty() || name.contains(['/', ':', '@']) {
return Err(bad_package());
}
let parts: Vec<&str> = version.split('.').collect();
let numeric = |part: &&str| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit());
if parts.len() != 3 || !parts.iter().all(numeric) {
return Err(WitLevelsError::BadVersion {
line,
package: package.to_string(),
version: version.to_string(),
});
}
Ok((package.to_string(), version.to_string()))
}
#[must_use]
pub fn level_has_phrase(level: u64, mismatch: &UnservedInterface) -> String {
let package = &mismatch.package;
match &mismatch.served_version {
Some(version) => format!("level {level} has {package}@{version}"),
None => format!("level {level} has no version of {package}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
const LEVEL_1: &str = "\
1 murmur:artifact-manager@0.1.0
1 murmur:capsule@0.1.0
1 murmur:conversation@0.2.0
1 murmur:driver@0.3.0
1 murmur:hook@0.9.0
1 murmur:host@0.1.0
1 murmur:message@0.1.0
1 murmur:runtime@0.4.0
1 murmur:runtime-guest@0.1.0
1 murmur:shell@0.1.0
1 murmur:stream@0.1.0
1 murmur:task@0.1.0
1 murmur:task-io@0.1.0
1 murmur:tool@0.1.0
1 murmur:tool-registry@0.1.0
";
fn two_levels() -> Vec<WitLevel> {
let level_2 = LEVEL_1
.lines()
.map(|line| {
line.replacen("1 ", "2 ", 1)
.replace("murmur:driver@0.3.0", "murmur:driver@0.4.0")
})
.collect::<Vec<_>>()
.join("\n");
parse_wit_levels(&format!("{LEVEL_1}{level_2}\n")).unwrap()
}
fn refusal(text: &str) -> WitLevelsError {
parse_wit_levels(text).unwrap_err()
}
#[test]
fn wit_level_reads_comments_blank_lines_and_levels() {
let levels = parse_wit_levels(&format!("# header\n\n{LEVEL_1} # trailing\n")).unwrap();
assert_eq!(levels.len(), 1);
assert_eq!(levels[0].level, 1);
assert_eq!(levels[0].packages.len(), 15);
assert_eq!(levels[0].version_of("murmur:driver"), Some("0.3.0"));
assert_eq!(levels[0].version_of("murmur:text"), None);
assert_eq!(two_levels()[1].version_of("murmur:driver"), Some("0.4.0"));
}
#[test]
fn wit_level_parser_refuses_each_malformed_line() {
assert_eq!(
refusal("# c\n1 murmur:a@0.1.0 extra\n"),
WitLevelsError::FieldCount { line: 2, fields: 3 }
);
assert_eq!(
refusal("1\n"),
WitLevelsError::FieldCount { line: 1, fields: 1 }
);
for bad in ["0", "-1", "01", "+1", "one"] {
assert_eq!(
refusal(&format!("{bad} murmur:a@0.1.0\n")),
WitLevelsError::NotALevel {
line: 1,
text: bad.to_string()
}
);
}
assert_eq!(
refusal("2 murmur:a@0.1.0\n"),
WitLevelsError::FirstLevelNotOne { line: 1, level: 2 }
);
assert_eq!(
refusal("1 murmur:a@0.1.0\n3 murmur:a@0.2.0\n"),
WitLevelsError::Gap {
line: 2,
level: 3,
previous: 1
}
);
assert_eq!(
refusal("1 murmur:a@0.1.0\n2 murmur:a@0.2.0\n1 murmur:b@0.1.0\n"),
WitLevelsError::OutOfOrder {
line: 3,
level: 1,
previous: 2
}
);
for bad in [
"wasi:a@0.1.0",
"murmur:a",
"murmur:@0.1.0",
"murmur:a/b@0.1.0",
] {
assert_eq!(
refusal(&format!("1 {bad}\n")),
WitLevelsError::BadPackage {
line: 1,
text: bad.to_string()
}
);
}
for bad in ["0.1", "0.1.0.0", "0.x.0", "0..0", "v0.1.0", "0.1.0-rc1"] {
assert_eq!(
refusal(&format!("1 murmur:a@{bad}\n")),
WitLevelsError::BadVersion {
line: 1,
package: "murmur:a".to_string(),
version: bad.to_string()
}
);
}
assert_eq!(
refusal("1 murmur:a@0.1.0\n1 murmur:a@0.2.0\n"),
WitLevelsError::DuplicatePackage {
line: 2,
level: 1,
package: "murmur:a".to_string()
}
);
assert_eq!(
refusal("1 murmur:b@0.1.0\n1 murmur:a@0.1.0\n"),
WitLevelsError::PackageOrder {
line: 2,
level: 1,
package: "murmur:a".to_string(),
previous: "murmur:b".to_string()
}
);
assert_eq!(refusal("# only a comment\n\n"), WitLevelsError::NoLevels);
assert_eq!(
refusal("1 murmur:a@0.1.0\n2 murmur:a@0.1.0\n"),
WitLevelsError::SameAsPrevious {
line: 2,
level: 2,
previous: 1
}
);
assert_eq!(
refusal("1 murmur:a@0.1.0\n2 murmur:a@0.1.0\n3 murmur:a@0.2.0\n"),
WitLevelsError::SameAsPrevious {
line: 2,
level: 2,
previous: 1
}
);
}
#[test]
fn wit_level_refusals_name_the_line() {
assert_eq!(
refusal("1 murmur:a@0.1.0\n1 murmur:b@x\n").to_string(),
"line 2: version 'x' of murmur:b is not <digits>.<digits>.<digits>"
);
}
}