kcode-dev-tools 0.2.0

Session-scoped tools for managed Rust libraries, Web libraries, and Rust binaries
Documentation
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};

use kcode_dev_tools::{
    CREATE_RUST_BIN_TOOL, CREATE_RUST_LIB_TOOL, CREATE_WEB_LIB_TOOL, Config, DOCS_RUST_BIN_TOOL,
    DOCS_RUST_LIB_TOOL, DOCS_WEB_LIB_TOOL, ManagedSourceKind, ObjectStore, ObjectStoreError,
    ObjectStoreResult, Service, WRITE_FILE_FREEFORM_RUST_BIN_TOOL,
    WRITE_FILE_FREEFORM_RUST_LIB_TOOL, WRITE_FILE_FREEFORM_WEB_LIB_TOOL,
};
use serde_json::json;

static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);

#[derive(Default)]
struct MemoryObjects {
    objects: Mutex<BTreeMap<String, Vec<u8>>>,
}

impl ObjectStore for MemoryObjects {
    fn load(&self, object_id: &str) -> ObjectStoreResult<Vec<u8>> {
        self.objects
            .lock()
            .unwrap()
            .get(object_id)
            .cloned()
            .ok_or_else(|| ObjectStoreError::new("not found"))
    }

    fn save(&self, bytes: &[u8]) -> ObjectStoreResult<String> {
        let mut objects = self.objects.lock().unwrap();
        let id = format!("object-{}", objects.len());
        objects.insert(id.clone(), bytes.to_vec());
        Ok(id)
    }
}

struct TestRoot(PathBuf);

impl TestRoot {
    fn new() -> Self {
        let counter = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
        let path = std::env::temp_dir().join(format!(
            "kcode-dev-tools-integration-{}-{counter}",
            std::process::id()
        ));
        fs::create_dir(&path).unwrap();
        Self(path)
    }

    fn path(&self) -> &Path {
        &self.0
    }
}

impl Drop for TestRoot {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.0);
    }
}

fn service(root: &Path) -> Service {
    Service::open(Config {
        rust_libraries_root: root.join("rust-libs"),
        web_libraries_root: root.join("web-libs"),
        web_publications_root: root.join("web-publications"),
        rust_binaries_root: root.join("rust-bins"),
        rust_binary_publications_root: root.join("rust-bin-publications"),
        crates_io_registry_token: "test-token".to_owned(),
        object_store: Arc::new(MemoryObjects::default()),
    })
    .unwrap()
}

#[tokio::test]
async fn one_service_manages_all_three_source_kinds_and_releases_together() {
    let root = TestRoot::new();
    let service = service(root.path());
    assert_eq!(
        service.web_publications_root(),
        fs::canonicalize(root.path().join("web-publications")).unwrap()
    );
    let session = "conversation:test";
    for (create, write, docs, name, path, kind) in [
        (
            CREATE_RUST_LIB_TOOL,
            WRITE_FILE_FREEFORM_RUST_LIB_TOOL,
            DOCS_RUST_LIB_TOOL,
            "demo-lib",
            "src/extra.rs",
            ManagedSourceKind::RustLibrary,
        ),
        (
            CREATE_WEB_LIB_TOOL,
            WRITE_FILE_FREEFORM_WEB_LIB_TOOL,
            DOCS_WEB_LIB_TOOL,
            "demo-web",
            "extra.js",
            ManagedSourceKind::WebLibrary,
        ),
        (
            CREATE_RUST_BIN_TOOL,
            WRITE_FILE_FREEFORM_RUST_BIN_TOOL,
            DOCS_RUST_BIN_TOOL,
            "demo-bin",
            "src/extra.rs",
            ManagedSourceKind::RustBinary,
        ),
    ] {
        let created = service
            .execute(session, create, json!({"name":name}))
            .await
            .unwrap();
        assert_eq!(created.snapshot.as_ref().unwrap().kind, kind);

        let written = service
            .execute(
                session,
                write,
                json!({"name":name,"path":path,"contents":"// managed\n"}),
            )
            .await
            .unwrap();
        assert!(written.snapshot.unwrap().text.contains(path));

        let documentation = service
            .execute(session, docs, json!({"name":name}))
            .await
            .unwrap();
        assert!(documentation.text.contains("Version: 0.1.0"));
    }

    assert_eq!(service.release(session).await.unwrap(), 3);
}

#[test]
fn web_source_and_publication_roots_must_be_disjoint() {
    let root = TestRoot::new();
    let shared = root.path().join("web");
    let error = match Service::open(Config {
        rust_libraries_root: root.path().join("rust-libs"),
        web_libraries_root: shared.clone(),
        web_publications_root: shared,
        rust_binaries_root: root.path().join("rust-bins"),
        rust_binary_publications_root: root.path().join("rust-bin-publications"),
        crates_io_registry_token: "test-token".to_owned(),
        object_store: Arc::new(MemoryObjects::default()),
    }) {
        Ok(_) => panic!("overlapping Web roots were accepted"),
        Err(error) => error,
    };
    assert_eq!(error.code, "dev_tools_invalid_root");
    assert_eq!(
        error.message,
        "Managed Web source and publication roots must be disjoint."
    );
}