daipendency-extractor-rust 0.5.0

Daipendency extractor for Rust library crates
Documentation
use daipendency_extractor::{LibraryMetadata, LibraryMetadataError};
use serde::{de::Error, Deserialize, Serialize};
use std::fs;
use std::path::Path;

const DEFAULT_LIB_PATH: &str = "src/lib.rs";
const README_PATH: &str = "README.md";

#[derive(Debug, Deserialize, Serialize)]
struct PackageConfig {
    name: String,
    #[serde(default, deserialize_with = "deserialize_version")]
    version: Option<String>,
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum VersionField {
    Direct(Option<String>),
    #[serde(rename = "workspace")]
    Workspace(serde::de::IgnoredAny),
}

fn deserialize_version<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    match VersionField::deserialize(deserializer) {
        Ok(VersionField::Direct(version)) => Ok(version),
        Ok(VersionField::Workspace(_)) => Ok(None),
        Err(e) => Err(D::Error::custom(format!("Malformed version field: {}", e))),
    }
}

#[derive(Debug, Deserialize, Serialize)]
struct LibConfig {
    path: Option<String>,
}

#[derive(Debug, Deserialize, Serialize)]
struct CargoConfig {
    package: PackageConfig,
    lib: Option<LibConfig>,
}

pub fn extract_metadata(path: &Path) -> Result<LibraryMetadata, LibraryMetadataError> {
    let cargo_toml_path = path.join("Cargo.toml");
    let cargo_toml_content =
        fs::read_to_string(&cargo_toml_path).map_err(LibraryMetadataError::MissingManifest)?;

    let cargo_config: CargoConfig = toml::from_str(&cargo_toml_content)
        .map_err(|e| LibraryMetadataError::MalformedManifest(format!("{}", e)))?;

    let readme_path = path.join(README_PATH);
    let documentation = fs::read_to_string(&readme_path).unwrap_or_default();

    let entry_point = cargo_config
        .lib
        .and_then(|lib| lib.path)
        .map(|path_str| path.join(Path::new(&path_str)))
        .unwrap_or_else(|| path.join(DEFAULT_LIB_PATH));

    Ok(LibraryMetadata {
        name: cargo_config.package.name,
        version: cargo_config.package.version,
        documentation,
        entry_point,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    fn create_test_crate(dir: &Path, custom_lib: Option<String>) -> Result<(), std::io::Error> {
        let config = CargoConfig {
            package: PackageConfig {
                name: "test-crate".to_string(),
                version: Some("0.1.0".to_string()),
            },
            lib: custom_lib.map(|path| LibConfig { path: Some(path) }),
        };

        let cargo_toml = toml::to_string(&config).unwrap();
        fs::write(dir.join("Cargo.toml"), cargo_toml)?;
        fs::write(dir.join(README_PATH), "Test crate")?;
        Ok(())
    }

    #[test]
    fn extract_metadata_valid_crate() {
        let temp_dir = TempDir::new().unwrap();
        create_test_crate(temp_dir.path(), None).unwrap();

        let result = extract_metadata(temp_dir.path());

        let metadata = result.unwrap();
        assert_eq!(metadata.name, "test-crate");
        assert_eq!(metadata.version, Some("0.1.0".to_string()));
        assert_eq!(metadata.documentation, "Test crate");
    }

    #[test]
    fn missing_cargo_toml() {
        let temp_dir = TempDir::new().unwrap();

        let result = extract_metadata(temp_dir.path());

        assert!(matches!(
            result,
            Err(LibraryMetadataError::MissingManifest(_))
        ));
    }

    #[test]
    fn missing_version() {
        let temp_dir = TempDir::new().unwrap();
        let config = CargoConfig {
            package: PackageConfig {
                name: "test-crate".to_string(),
                version: None,
            },
            lib: None,
        };
        fs::write(
            temp_dir.path().join("Cargo.toml"),
            toml::to_string(&config).unwrap(),
        )
        .unwrap();
        fs::write(temp_dir.path().join(README_PATH), "Test crate").unwrap();

        let result = extract_metadata(temp_dir.path()).unwrap();

        assert_eq!(result.version, None);
    }

    #[test]
    fn invalid_cargo_toml() {
        let temp_dir = TempDir::new().unwrap();
        fs::write(temp_dir.path().join("Cargo.toml"), "invalid toml content").unwrap();

        let result = extract_metadata(temp_dir.path());

        assert!(matches!(
            result,
            Err(LibraryMetadataError::MalformedManifest(_))
        ));
    }

    #[test]
    fn missing_package_section() {
        let temp_dir = TempDir::new().unwrap();
        fs::write(
            temp_dir.path().join("Cargo.toml"),
            "[dependencies]\nfoo = \"1.0\"",
        )
        .unwrap();

        let result = extract_metadata(temp_dir.path());

        assert!(matches!(
            result,
            Err(LibraryMetadataError::MalformedManifest(_))
        ));
    }

    #[test]
    fn missing_readme() {
        let temp_dir = TempDir::new().unwrap();
        create_test_crate(temp_dir.path(), None).unwrap();
        fs::remove_file(temp_dir.path().join(README_PATH)).unwrap();

        let result = extract_metadata(temp_dir.path());

        assert!(result.is_ok());
        assert_eq!(result.unwrap().documentation, "");
    }

    #[test]
    fn workspace_version() {
        let temp_dir = TempDir::new().unwrap();
        let cargo_toml = r#"
[package]
name = "test-crate"
version.workspace = true
"#;
        fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml).unwrap();
        fs::write(temp_dir.path().join(README_PATH), "Test crate").unwrap();

        let metadata = extract_metadata(temp_dir.path()).unwrap();

        assert_eq!(metadata.version, None);
    }

    mod entrypoint {
        use super::*;

        #[test]
        fn default_entry_point() {
            let temp_dir = TempDir::new().unwrap();

            create_test_crate(temp_dir.path(), None).unwrap();

            let metadata = extract_metadata(temp_dir.path()).unwrap();

            assert_eq!(metadata.entry_point, temp_dir.path().join(DEFAULT_LIB_PATH));
        }

        #[test]
        fn custom_entry_point() {
            let temp_dir = TempDir::new().unwrap();
            let custom_lib_path = "src/custom_lib.rs";
            create_test_crate(temp_dir.path(), Some(custom_lib_path.to_string())).unwrap();

            let metadata = extract_metadata(temp_dir.path()).unwrap();

            assert_eq!(metadata.entry_point, temp_dir.path().join(custom_lib_path));
        }
    }
}