projd-core 0.6.1

Core project scanning data model and analysis helpers for Projd.
Documentation
use std::fs;
use std::path::Path;

use projd_core::{IdentitySource, ProjectKind, render_markdown, scan_path};
use tempfile::TempDir;

#[test]
fn detects_rust_package_identity_from_cargo_manifest() {
    let fixture = ProjectFixture::new();
    fixture.write(
        "Cargo.toml",
        "[package]\nname = \"demo-rust\"\nversion = \"1.2.3\"\nedition = \"2024\"\n",
    );

    let scan = scan_path(fixture.path()).expect("scan succeeds");

    assert_eq!(scan.identity.name, "demo-rust");
    assert_eq!(scan.identity.version.as_deref(), Some("1.2.3"));
    assert_eq!(scan.identity.kind, ProjectKind::RustPackage);
    assert_eq!(scan.identity.source, IdentitySource::CargoToml);
}

#[test]
fn detects_rust_workspace_identity_from_workspace_manifest() {
    let fixture = ProjectFixture::new();
    fixture.write(
        "Cargo.toml",
        "[workspace]\nmembers = [\"crates/demo\"]\n[workspace.package]\nversion = \"0.4.0\"\n",
    );

    let scan = scan_path(fixture.path()).expect("scan succeeds");

    assert_eq!(scan.identity.name, fixture.name());
    assert_eq!(scan.identity.version.as_deref(), Some("0.4.0"));
    assert_eq!(scan.identity.kind, ProjectKind::RustWorkspace);
    assert_eq!(scan.identity.source, IdentitySource::CargoToml);
}

#[test]
fn detects_node_package_identity_from_package_json() {
    let fixture = ProjectFixture::new();
    fixture.write(
        "package.json",
        "{\"name\":\"demo-node\",\"version\":\"2.0.1\"}\n",
    );

    let scan = scan_path(fixture.path()).expect("scan succeeds");

    assert_eq!(scan.identity.name, "demo-node");
    assert_eq!(scan.identity.version.as_deref(), Some("2.0.1"));
    assert_eq!(scan.identity.kind, ProjectKind::NodePackage);
    assert_eq!(scan.identity.source, IdentitySource::PackageJson);
}

#[test]
fn detects_python_project_identity_from_pyproject_toml() {
    let fixture = ProjectFixture::new();
    fixture.write(
        "pyproject.toml",
        "[project]\nname = \"demo-python\"\nversion = \"3.1.4\"\n",
    );

    let scan = scan_path(fixture.path()).expect("scan succeeds");

    assert_eq!(scan.identity.name, "demo-python");
    assert_eq!(scan.identity.version.as_deref(), Some("3.1.4"));
    assert_eq!(scan.identity.kind, ProjectKind::PythonProject);
    assert_eq!(scan.identity.source, IdentitySource::PyprojectToml);
}

#[test]
fn falls_back_to_directory_identity() {
    let fixture = ProjectFixture::new();
    fixture.write("README.md", "# Generic\n");

    let scan = scan_path(fixture.path()).expect("scan succeeds");

    assert_eq!(scan.identity.name, fixture.name());
    assert_eq!(scan.identity.version, None);
    assert_eq!(scan.identity.kind, ProjectKind::Generic);
    assert_eq!(scan.identity.source, IdentitySource::DirectoryName);
}

#[test]
fn renders_identity_in_markdown_report() {
    let fixture = ProjectFixture::new();
    fixture.write(
        "Cargo.toml",
        "[package]\nname = \"demo-rust\"\nversion = \"1.2.3\"\nedition = \"2024\"\n",
    );

    let scan = scan_path(fixture.path()).expect("scan succeeds");
    let report = render_markdown(&scan);

    assert!(report.contains("## Identity"));
    assert!(report.contains("- Name: `demo-rust`"));
    assert!(report.contains("- Version: `1.2.3`"));
    assert!(report.contains("- Kind: RustPackage"));
    assert!(report.contains("- Source: CargoToml"));
}

struct ProjectFixture {
    temp_dir: TempDir,
}

impl ProjectFixture {
    fn new() -> Self {
        Self {
            temp_dir: tempfile::tempdir().expect("create temp dir"),
        }
    }

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

    fn name(&self) -> String {
        self.path()
            .file_name()
            .and_then(|value| value.to_str())
            .expect("temp dir has name")
            .to_owned()
    }

    fn write(&self, relative: &str, content: &str) {
        let path = self.path().join(relative);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).expect("create fixture parent");
        }
        fs::write(path, content).expect("write fixture file");
    }
}