cargo_declared/
metadata.rs1use 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}