kcode-k1-web-code-document 0.3.0

Represent versionless authored K1 Web source trees
Documentation
use kcode_k1_transaction_id::TxId;
use kcode_k1_web_code_document::{CodeDocument, Language};
use kcode_k1_web_package::{AuthorityId, SourceFile, WebFamily};
use semver::Version;

fn family() -> WebFamily {
    WebFamily::new(AuthorityId::new(TxId::from_bytes([1; 12])), "alpha").unwrap()
}

#[test]
fn templates_are_exact_two_file_authored_trees() {
    for (language, path) in [
        (Language::JavaScript, "index.js"),
        (Language::Html, "index.html"),
        (Language::Css, "index.css"),
    ] {
        let document = CodeDocument::template(family(), language).unwrap();
        assert_eq!(
            document
                .files()
                .iter()
                .map(|file| file.path())
                .collect::<Vec<_>>(),
            vec!["Documentation.md", path]
        );
        assert!(document.file("k1-web.json").is_none());
        assert!(
            document
                .files()
                .iter()
                .all(|file| !file.path().starts_with("Code."))
        );

        let package = document.to_source_package(Version::new(1, 2, 3)).unwrap();
        assert_eq!(package.id().version(), &Version::new(1, 2, 3));
        assert!(
            package
                .files()
                .iter()
                .any(|file| file.path() == "k1-web.json")
        );
        if language == Language::JavaScript {
            assert_eq!((package.entry(), package.tests()), ("index.js", "index.js"));
        } else {
            assert_eq!(
                (package.entry(), package.tests()),
                ("k1-entry.js", "k1-tests.js")
            );
            let adapter = package
                .files()
                .iter()
                .find(|file| file.path() == "k1-entry.js")
                .unwrap();
            assert!(std::str::from_utf8(adapter.bytes()).unwrap().contains(path));
        }
    }
}

#[test]
fn full_trees_and_text_mutations_preserve_exact_bytes() {
    let original = CodeDocument::new(
        family(),
        vec![
            SourceFile::new("src/unusual.data", b"old".to_vec()),
            SourceFile::new("assets/icon.png", vec![0, 159, 255]),
        ],
    )
    .unwrap();

    let created = original
        .create_text("src/new.txt", "created\n".into())
        .unwrap();
    assert!(original.file("src/new.txt").is_none());
    assert_eq!(created.file("src/new.txt").unwrap().bytes(), b"created\n");

    let replaced = created
        .replace_text("src/unusual.data", "new".into())
        .unwrap();
    assert_eq!(replaced.file("src/unusual.data").unwrap().bytes(), b"new");
    assert_eq!(created.file("src/unusual.data").unwrap().bytes(), b"old");

    let error = replaced
        .replace_text("assets/icon.png", "forbidden".into())
        .unwrap_err();
    assert_eq!(
        error.message(),
        "this file is a non-text object and cannot be modified"
    );

    let renamed = replaced
        .rename("assets/icon.png", "assets/logo.png")
        .unwrap();
    assert!(renamed.file("assets/icon.png").is_none());
    assert_eq!(
        renamed.file("assets/logo.png").unwrap().bytes(),
        [0, 159, 255]
    );
    assert!(
        renamed
            .delete("assets/logo.png")
            .unwrap()
            .file("assets/logo.png")
            .is_none()
    );
}

#[test]
fn explicit_manifest_projection_changes_only_the_version_token() {
    let manifest = b"{\n  \"name\":\"alpha\", \"version\" : \"0.0.0\",\n  \"entry\":\"index.js\",\"tests\":\"index.js\",\"dependencies\":[]\n}\n";
    let document = CodeDocument::new(
        family(),
        vec![
            SourceFile::new("Documentation.md", b"docs\r\n".to_vec()),
            SourceFile::new(
                "index.js",
                b"export async function runTests() {}\n".to_vec(),
            ),
            SourceFile::new("k1-web.json", manifest.to_vec()),
            SourceFile::new("assets/blob.bin", vec![0, 255, 1]),
        ],
    )
    .unwrap();

    let package = document.to_source_package(Version::new(2, 3, 4)).unwrap();
    let projected = package
        .files()
        .iter()
        .find(|file| file.path() == "k1-web.json")
        .unwrap();
    assert_eq!(
        std::str::from_utf8(projected.bytes()).unwrap(),
        std::str::from_utf8(manifest)
            .unwrap()
            .replace("\"0.0.0\"", "\"2.3.4\"")
    );
    assert_eq!(
        package
            .files()
            .iter()
            .find(|file| file.path() == "assets/blob.bin")
            .unwrap()
            .bytes(),
        [0, 255, 1]
    );

    let recovered = CodeDocument::from_source_package(&package).unwrap();
    assert_eq!(recovered.files(), package.files());
}

#[test]
fn tree_shape_is_validated_while_package_validity_is_deferred() {
    let temporary = CodeDocument::new(
        family(),
        vec![SourceFile::new("nested/readme.odd", b"draft".to_vec())],
    )
    .unwrap();
    assert!(temporary.to_source_package(Version::new(1, 0, 0)).is_err());

    for files in [
        vec![
            SourceFile::new("same", Vec::new()),
            SourceFile::new("same", Vec::new()),
        ],
        vec![SourceFile::new("../outside", Vec::new())],
        vec![
            SourceFile::new("assets", Vec::new()),
            SourceFile::new("assets/icon.png", Vec::new()),
        ],
    ] {
        assert!(CodeDocument::new(family(), files).is_err());
    }
}

#[test]
fn mutations_reject_missing_colliding_and_ambiguous_paths() {
    let document = CodeDocument::template(family(), Language::JavaScript).unwrap();
    assert!(
        document
            .create_text("index.js", "duplicate".into())
            .is_err()
    );
    assert!(document.rename("index.js", "Documentation.md").is_err());
    assert!(document.rename("index.js", "index.js").is_err());
    assert!(document.rename("missing", "other").is_err());
    assert!(document.delete("missing").is_err());

    let ambiguous = CodeDocument::new(
        family(),
        vec![
            SourceFile::new("Documentation.md", b"docs".to_vec()),
            SourceFile::new("index.js", b"export function runTests() {}".to_vec()),
            SourceFile::new("index.html", b"<p>x</p>".to_vec()),
        ],
    )
    .unwrap();
    assert!(ambiguous.to_source_package(Version::new(1, 0, 0)).is_err());
}