kcode-k1-web-package 0.1.0

Validate authority-namespaced buildless K1 Web package source
Documentation
use kcode_k1_transaction_id::TxId;
use kcode_k1_web_package::{
    AuthorityId, DependencySelector, SourceFile, SourcePackage, WebFamily, WebId,
};
use semver::Version;

fn authority(byte: u8) -> AuthorityId {
    AuthorityId::new(TxId::from_bytes([byte; 12]))
}

fn package_id() -> WebId {
    WebId::new(
        WebFamily::new(authority(1), "alpha").unwrap(),
        Version::new(1, 2, 3),
    )
    .unwrap()
}

fn manifest(name: &str, version: &str, entry: &str, tests: &str, dependencies: &str) -> String {
    format!(
        r#"{{"name":"{name}","version":"{version}","entry":"{entry}","tests":"{tests}","dependencies":{dependencies}}}"#
    )
}

fn complete_files(manifest: String) -> Vec<SourceFile> {
    vec![
        SourceFile::new("tests.js", b"export function runTests() {}".to_vec()),
        SourceFile::new("k1-web.json", manifest.into_bytes()),
        SourceFile::new("index.js", b"export {};".to_vec()),
        SourceFile::new("Documentation.md", b"contract".to_vec()),
    ]
}

fn standard_manifest(dependencies: &str) -> String {
    manifest("alpha", "1.2.3", "index.js", "tests.js", dependencies)
}

fn dependency(authority: &str, name: &str, selector: &str) -> String {
    format!(r#"{{"authority":"{authority}","name":"{name}","selector":"{selector}"}}"#)
}

fn dependency_list(dependencies: &[String]) -> String {
    format!("[{}]", dependencies.join(","))
}

#[test]
fn validates_names_versions_and_selectors() {
    let family = WebFamily::new(authority(1), "a-b2").unwrap();
    assert_eq!(
        (family.authority(), family.logical_name()),
        (authority(1), "a-b2")
    );
    for name in ["", "-a", "a-", "a--b", "A", "a_b", "é"] {
        assert!(WebFamily::new(authority(1), name).is_err(), "{name}");
    }
    assert!(WebFamily::new(authority(1), "a".repeat(37)).is_err());
    for version in ["1.2.3-pre", "1.2.3+build"] {
        assert!(WebId::new(family.clone(), Version::parse(version).unwrap()).is_err());
    }
    for text in ["*", "0", "0.2", "0.2.3"] {
        assert_eq!(DependencySelector::parse(text).unwrap().to_string(), text);
    }
    assert!(
        DependencySelector::parse("0")
            .unwrap()
            .matches(&Version::new(0, 8, 9))
    );
    for text in ["", "01", "1.02", "1.2.03", "1.2.3.4", "v1", "1.*"] {
        assert!(DependencySelector::parse(text).is_err(), "{text}");
    }
    assert!(
        !DependencySelector::parse("*")
            .unwrap()
            .matches(&Version::parse("1.2.3-pre").unwrap())
    );
}

#[test]
fn validates_paths_utf8_and_arbitrary_assets() {
    for path in ["", "/a", "a//b", "a/./b", "a/../b", "a\\b", "a:b"] {
        let mut files = complete_files(standard_manifest("[]"));
        files.push(SourceFile::new(path, Vec::new()));
        assert!(SourcePackage::new(package_id(), files).is_err(), "{path}");
    }
    let mut duplicate = complete_files(standard_manifest("[]"));
    duplicate.push(SourceFile::new("index.js", Vec::new()));
    assert!(SourcePackage::new(package_id(), duplicate).is_err());
    let mut collision = complete_files(standard_manifest("[]"));
    collision.push(SourceFile::new("assets", Vec::new()));
    collision.push(SourceFile::new("assets/icon.bin", Vec::new()));
    assert!(SourcePackage::new(package_id(), collision).is_err());
    for path in ["k1-web.json", "Documentation.md", "index.js", "tests.js"] {
        let mut files = complete_files(standard_manifest("[]"));
        *files.iter_mut().find(|file| file.path() == path).unwrap() =
            SourceFile::new(path, vec![0xff]);
        assert!(SourcePackage::new(package_id(), files).is_err(), "{path}");
    }
    let bytes = vec![0, 159, 255];
    let mut files = complete_files(standard_manifest("[]"));
    files.push(SourceFile::new("z/asset.bin", bytes.clone()));
    let package = SourcePackage::new(package_id(), files).unwrap();
    assert!(package.files().windows(2).all(|pair| pair[0] < pair[1]));
    assert_eq!(package.files().last().unwrap().bytes(), bytes);
}

#[test]
fn enforces_exact_manifest_and_javascript_targets() {
    let valid = standard_manifest("[]");
    let invalid = [
        valid.replace("\"dependencies\":[]", "\"dependencies\":[],\"extra\":true"),
        valid.replace(",\"tests\":\"tests.js\"", ""),
        manifest("other", "1.2.3", "index.js", "tests.js", "[]"),
        manifest("alpha", "1.2.4", "index.js", "tests.js", "[]"),
        manifest("alpha", "01.2.3", "index.js", "tests.js", "[]"),
        manifest("alpha", "1.2.3", "index.txt", "tests.js", "[]"),
        manifest("alpha", "1.2.3", "index.js", "missing.mjs", "[]"),
    ];
    for manifest in invalid {
        assert!(SourcePackage::new(package_id(), complete_files(manifest)).is_err());
    }
    let package = SourcePackage::new(package_id(), complete_files(valid)).unwrap();
    assert_eq!((package.entry(), package.tests()), ("index.js", "tests.js"));
}

#[test]
fn validates_deduplicates_and_sorts_dependencies() {
    let first = authority(0xab).to_string();
    let second = authority(0xcd).to_string();
    let declarations = dependency_list(&[
        dependency(&second, "alpha", "*"),
        dependency(&first, "zeta", "2"),
        dependency(&first, "alpha", "1.2.3"),
        dependency(&first, "alpha", "1"),
        dependency(&first, "alpha", "0"),
        dependency(&first, "alpha", "*"),
    ]);
    let package = SourcePackage::new(
        package_id(),
        complete_files(standard_manifest(&declarations)),
    )
    .unwrap();
    let dependencies = package.dependencies();
    let selectors = dependencies
        .iter()
        .map(|item| item.selector().to_string())
        .collect::<Vec<_>>();
    assert_eq!(selectors, ["*", "0", "1", "1.2.3", "2", "*"]);
    assert_eq!(dependencies[0].authority().to_string(), first);
    assert_eq!(dependencies[4].name(), "zeta");
    assert_eq!(dependencies[5].authority().to_string(), second);

    let duplicate = dependency(&first, "alpha", "*");
    let unknown = duplicate.replace('}', ",\"extra\":true}");
    for declarations in [
        dependency_list(&[duplicate.clone(), duplicate]),
        dependency_list(&[dependency(&first.to_uppercase(), "alpha", "*")]),
        dependency_list(&[unknown]),
    ] {
        let files = complete_files(standard_manifest(&declarations));
        assert!(SourcePackage::new(package_id(), files).is_err());
    }
}