use crate::{
wit_contracts_from_artifact_bytes, ArtifactMeta, IndexedVersion, UnservedInterface,
WitContracts, WitLevel,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionFit {
Fits,
Unserved(Vec<UnservedInterface>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FitSubject {
pub contracts: WitContracts,
}
impl FitSubject {
#[must_use]
pub fn component(contracts: WitContracts) -> Self {
Self { contracts }
}
#[must_use]
pub fn indexed(version: &IndexedVersion) -> Self {
Self {
contracts: WitContracts {
exports: Vec::new(),
imports: version.interfaces.clone(),
},
}
}
#[must_use]
pub fn stored(meta: &ArtifactMeta, bytes: Option<&[u8]>) -> Self {
let contracts = meta
.wit_contracts
.clone()
.or_else(|| bytes.and_then(wit_contracts_from_artifact_bytes))
.unwrap_or_default();
Self { contracts }
}
}
#[must_use]
pub fn version_fit(subject: &FitSubject, level: &WitLevel) -> VersionFit {
let unserved = subject.contracts.unserved_against(&level.served());
if unserved.is_empty() {
VersionFit::Fits
} else {
VersionFit::Unserved(unserved)
}
}
#[must_use]
pub fn newest_fitting_level(contracts: &WitContracts, levels: &[WitLevel]) -> Option<u64> {
let subject = FitSubject::component(contracts.clone());
levels
.iter()
.filter(|level| version_fit(&subject, level) == VersionFit::Fits)
.map(|level| level.level)
.max()
}
#[cfg(test)]
mod tests {
use std::io::{Cursor, Write};
use zip::{write::SimpleFileOptions, ZipWriter};
use super::*;
use crate::{
level_has_phrase, parse_wit_levels, ArtifactName, ContractDirection, IndexedAsset,
RuntimeType,
};
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 contracts(exports: &[&str], imports: &[&str]) -> WitContracts {
WitContracts {
exports: exports.iter().map(|s| s.to_string()).collect(),
imports: imports.iter().map(|s| s.to_string()).collect(),
}
}
fn levels_accepting(contracts: &WitContracts, levels: &[WitLevel]) -> Vec<u64> {
let subject = FitSubject::component(contracts.clone());
levels
.iter()
.filter(|level| version_fit(&subject, level) == VersionFit::Fits)
.map(|level| level.level)
.collect()
}
fn meta(wit_contracts: Option<WitContracts>) -> ArtifactMeta {
ArtifactMeta {
name: ArtifactName::parse("ana/tool-x").unwrap(),
version: "0.1.0".to_string(),
runtime: RuntimeType::Wasm,
artifact_runtime: "tool".to_string(),
description: None,
tags: Vec::new(),
wit_contracts,
owner_id: None,
built_at_level: None,
}
}
fn artifact_importing(names: &[&str]) -> Vec<u8> {
let imports: String = names
.iter()
.map(|name| format!("(import \"{name}\" (instance))"))
.collect();
let mut buf = Vec::new();
{
let mut zip = ZipWriter::new(Cursor::new(&mut buf));
zip.start_file("capsule.wasm", SimpleFileOptions::default())
.unwrap();
zip.write_all(&wat::parse_str(format!("(component {imports})")).unwrap())
.unwrap();
zip.finish().unwrap();
}
buf
}
#[test]
fn fit_an_interface_mismatch_is_unserved() {
let levels = two_levels();
let subject = FitSubject::component(contracts(&["murmur:driver/process@0.3.0"], &[]));
let VersionFit::Unserved(unserved) = version_fit(&subject, &levels[1]) else {
panic!("a 0.3.0 driver fits a level serving murmur:driver@0.4.0");
};
assert_eq!(unserved[0].package, "murmur:driver");
}
#[test]
fn fit_empty_contracts_fit_every_level() {
let levels = two_levels();
let skill = WitContracts::default();
assert_eq!(levels_accepting(&skill, &levels), vec![1, 2]);
let wasi_only = contracts(&[], &["wasi:cli/stdout@0.2.0"]);
assert_eq!(newest_fitting_level(&wasi_only, &levels), Some(2));
}
#[test]
fn fit_indexed_reads_interfaces() {
let version = IndexedVersion {
version: "1.0.0".to_string(),
release: "v1".to_string(),
built_at_level: None,
interfaces: vec!["murmur:tool/run@0.1.0".to_string()],
assets: vec![IndexedAsset {
file: "tool-x-1.0.0.mur.zip".to_string(),
sha256: "a".repeat(64),
}],
};
let subject = FitSubject::indexed(&version);
assert_eq!(subject.contracts.imports, version.interfaces);
}
#[test]
fn fit_stored_prefers_sidecar_contracts_then_bytes() {
let bytes = artifact_importing(&["murmur:tool/run@0.1.0"]);
let sidecar = contracts(&["murmur:hook/lifecycle@0.9.0"], &[]);
let from_sidecar = FitSubject::stored(&meta(Some(sidecar.clone())), Some(&bytes));
assert_eq!(from_sidecar.contracts, sidecar);
let from_bytes = FitSubject::stored(&meta(None), Some(&bytes));
assert_eq!(
from_bytes.contracts,
contracts(&[], &["murmur:tool/run@0.1.0"])
);
let neither = FitSubject::stored(&meta(None), None);
assert_eq!(neither.contracts, WitContracts::default());
let opaque = FitSubject::stored(&meta(None), Some(b"not a zip"));
assert_eq!(opaque.contracts, WitContracts::default());
}
#[test]
fn fit_a_tool_naming_no_driver_interface_fits_both_levels() {
let levels = two_levels();
let tool = contracts(&["murmur:tool/run@0.1.0"], &[]);
assert_eq!(levels_accepting(&tool, &levels), vec![1, 2]);
assert_eq!(newest_fitting_level(&tool, &levels), Some(2));
}
#[test]
fn fit_a_process_driver_fits_only_the_level_it_was_built_against() {
let levels = two_levels();
let driver = contracts(&["murmur:driver/process@0.3.0"], &[]);
let subject = FitSubject::component(driver.clone());
assert_eq!(version_fit(&subject, &levels[0]), VersionFit::Fits);
let VersionFit::Unserved(mismatched) = version_fit(&subject, &levels[1]) else {
panic!("a 0.3.0 driver fits a level serving murmur:driver@0.4.0");
};
assert_eq!(mismatched.len(), 1);
assert_eq!(mismatched[0].direction, ContractDirection::Export);
assert_eq!(mismatched[0].package, "murmur:driver");
assert_eq!(mismatched[0].served_version.as_deref(), Some("0.4.0"));
assert_eq!(
level_has_phrase(2, &mismatched[0]),
"level 2 has murmur:driver@0.4.0"
);
assert_eq!(levels_accepting(&driver, &levels), vec![1]);
assert_eq!(newest_fitting_level(&driver, &levels), Some(1));
}
#[test]
fn fit_a_hook_at_an_older_version_fits_no_level() {
let levels = two_levels();
let hook = contracts(&["murmur:hook/lifecycle@0.8.0"], &[]);
assert_eq!(levels_accepting(&hook, &levels), Vec::<u64>::new());
assert_eq!(newest_fitting_level(&hook, &levels), None);
let text = FitSubject::component(contracts(&[], &["murmur:text/stream@0.1.0"]));
let VersionFit::Unserved(mismatched) = version_fit(&text, &levels[0]) else {
panic!("murmur:text fits level 1");
};
assert_eq!(
level_has_phrase(1, &mismatched[0]),
"level 1 has no version of murmur:text"
);
}
}