Skip to main content

cargo_declared/
metadata.rs

1use cargo_metadata::{DependencyKind as CargoDependencyKind, Metadata, MetadataCommand, Package};
2use serde::Serialize;
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6use crate::error::{Error, Result};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum DependencyKind {
11    Normal,
12    Development,
13    Build,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
17pub struct DependencyInfo {
18    pub name: String,
19    pub version: Option<String>,
20    pub source: Option<String>,
21    pub kind: DependencyKind,
22}
23
24#[derive(Debug, Clone)]
25pub struct ParsedMetadata {
26    pub workspace_root: PathBuf,
27    pub package_name: String,
28    pub declared_deps: Vec<DependencyInfo>,
29    pub compiled_deps: Vec<DependencyInfo>,
30    pub package_graph: HashMap<String, Vec<String>>,
31}
32
33pub fn parse_metadata(path: Option<PathBuf>) -> Result<ParsedMetadata> {
34    let metadata = load_metadata(path.as_deref())?;
35    let resolve = metadata.resolve.as_ref().ok_or(Error::NoRootPackage)?;
36    let root_id = resolve.root.as_ref().ok_or(Error::NoRootPackage)?;
37    let root_pkg = find_package(&metadata, root_id).ok_or(Error::NoRootPackage)?;
38
39    Ok(ParsedMetadata {
40        workspace_root: metadata.workspace_root.clone().into(),
41        package_name: root_pkg.name.clone(),
42        declared_deps: root_pkg.dependencies.iter().map(map_declared_dep).collect(),
43        compiled_deps: collect_compiled_deps(&metadata, root_id),
44        package_graph: build_package_graph(&metadata),
45    })
46}
47
48fn load_metadata(path: Option<&Path>) -> Result<Metadata> {
49    let mut command = MetadataCommand::new();
50
51    if let Some(path) = path {
52        if !path.exists() {
53            return Err(Error::PathNotFound {
54                path: path.to_path_buf(),
55            });
56        }
57
58        if path.file_name().is_some_and(|name| name == "Cargo.toml") {
59            command.manifest_path(path);
60        } else {
61            command.current_dir(path);
62        }
63    }
64
65    Ok(command.exec()?)
66}
67
68fn find_package<'a>(
69    metadata: &'a Metadata,
70    package_id: &cargo_metadata::PackageId,
71) -> Option<&'a Package> {
72    metadata.packages.iter().find(|pkg| &pkg.id == package_id)
73}
74
75fn map_declared_dep(dep: &cargo_metadata::Dependency) -> DependencyInfo {
76    DependencyInfo {
77        name: dep.rename.clone().unwrap_or_else(|| dep.name.clone()),
78        version: Some(dep.req.to_string()),
79        source: dep
80            .path
81            .as_ref()
82            .map(|path| format!("path+{}", path))
83            .or_else(|| dep.registry.clone()),
84        kind: map_kind(dep.kind),
85    }
86}
87
88fn map_kind(kind: cargo_metadata::DependencyKind) -> DependencyKind {
89    match kind {
90        CargoDependencyKind::Development => DependencyKind::Development,
91        CargoDependencyKind::Build => DependencyKind::Build,
92        CargoDependencyKind::Normal | CargoDependencyKind::Unknown => DependencyKind::Normal,
93    }
94}
95
96fn collect_compiled_deps(
97    metadata: &Metadata,
98    root_id: &cargo_metadata::PackageId,
99) -> Vec<DependencyInfo> {
100    let Some(resolve) = metadata.resolve.as_ref() else {
101        return Vec::new();
102    };
103
104    resolve
105        .nodes
106        .iter()
107        .filter(|node| &node.id != root_id)
108        .filter_map(|node| find_package(metadata, &node.id))
109        .map(|pkg| DependencyInfo {
110            name: pkg.name.clone(),
111            version: Some(pkg.version.to_string()),
112            source: pkg.source.as_ref().map(ToString::to_string),
113            kind: DependencyKind::Normal,
114        })
115        .collect()
116}
117
118fn build_package_graph(metadata: &Metadata) -> HashMap<String, Vec<String>> {
119    let id_to_name: HashMap<_, _> = metadata
120        .packages
121        .iter()
122        .map(|pkg| (pkg.id.clone(), pkg.name.clone()))
123        .collect();
124
125    let mut graph = HashMap::new();
126
127    if let Some(resolve) = &metadata.resolve {
128        for node in &resolve.nodes {
129            let Some(name) = id_to_name.get(&node.id) else {
130                continue;
131            };
132
133            let deps = node
134                .deps
135                .iter()
136                .map(|dep| dep.name.clone())
137                .collect::<Vec<_>>();
138
139            graph.insert(name.clone(), deps);
140        }
141    }
142
143    graph
144}