callisto_graph/
identity.rs1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use callisto_model::{Ecosystem, ManifestFormat, PackageId};
5
6use crate::error::GraphError;
7
8pub struct IdentityResolver {
9 workspace_root: PathBuf,
10}
11
12impl IdentityResolver {
13 pub fn new(workspace_root: &Path) -> Result<Self, GraphError> {
14 Ok(IdentityResolver {
15 workspace_root: workspace_root.to_path_buf(),
16 })
17 }
18
19 pub fn resolve(
20 &self,
21 project_root: &Path,
22 ecosystem: Ecosystem,
23 ) -> Result<PackageId, GraphError> {
24 let abs = self.workspace_root.join(project_root);
25 let name = match ecosystem {
26 Ecosystem::Cargo => {
27 let cargo_toml = abs.join("Cargo.toml");
28 let content = std::fs::read_to_string(&cargo_toml).map_err(|e| {
29 callisto_model::ManifestError::Read {
30 path: project_root.join("Cargo.toml"),
31 message: e.to_string(),
32 }
33 })?;
34 let doc: toml_edit::DocumentMut =
35 content.parse().map_err(|e: toml_edit::TomlError| {
36 callisto_model::ManifestError::Parse {
37 path: project_root.join("Cargo.toml"),
38 format: ManifestFormat::CargoToml,
39 message: e.to_string(),
40 }
41 })?;
42 doc.get("package")
43 .and_then(|p| p.get("name"))
44 .and_then(|n| n.as_str())
45 .ok_or_else(|| callisto_model::ManifestError::MissingField {
46 path: project_root.join("Cargo.toml"),
47 field: "package.name",
48 })?
49 .to_string()
50 }
51 Ecosystem::Npm => {
52 let pkg_json = abs.join("package.json");
53 let content = std::fs::read_to_string(&pkg_json).map_err(|e| {
54 callisto_model::ManifestError::Read {
55 path: project_root.join("package.json"),
56 message: e.to_string(),
57 }
58 })?;
59 let val: serde_json::Value = serde_json::from_str(&content).map_err(|e| {
60 callisto_model::ManifestError::Parse {
61 path: project_root.join("package.json"),
62 format: ManifestFormat::PackageJson,
63 message: e.to_string(),
64 }
65 })?;
66 val.get("name")
67 .and_then(|n| n.as_str())
68 .ok_or_else(|| callisto_model::ManifestError::MissingField {
69 path: project_root.join("package.json"),
70 field: "name",
71 })?
72 .to_string()
73 }
74 _ => {
75 return Err(GraphError::AmbiguousName {
76 name: "unsupported ecosystem".to_string(),
77 candidates: Vec::new(),
78 });
79 }
80 };
81
82 PackageId::parse(&name).map_err(|_err| GraphError::AmbiguousName {
83 name: name.clone(),
84 candidates: Vec::new(),
85 })
86 }
87}
88
89#[derive(Clone, Debug, Default)]
90pub struct IdentityIndex {
91 pub bare: BTreeMap<String, PackageId>,
92 pub prefixed: BTreeMap<(Ecosystem, String), PackageId>,
93 pub native: BTreeMap<(Ecosystem, String), PackageId>,
94 pub platform: BTreeMap<String, (PackageId, PathBuf)>,
95}
96
97impl IdentityIndex {
98 pub fn resolve_human(
99 &self,
100 name: &str,
101 siblings: &[PackageId],
102 ) -> Result<PackageId, GraphError> {
103 if let Ok(id) = PackageId::parse(name) {
104 if self.bare.values().any(|v| v == &id) || self.prefixed.values().any(|v| v == &id) {
105 return Ok(id);
106 }
107 }
108
109 if let Some(id) = self.bare.get(name) {
110 return Ok(id.clone());
111 }
112
113 let mut candidates = Vec::new();
114 for ((_eco, n), id) in &self.prefixed {
115 if n == name {
116 candidates.push(id.clone());
117 }
118 }
119
120 if candidates.len() == 1 {
121 return Ok(candidates[0].clone());
122 }
123
124 if candidates.len() > 1 && !siblings.is_empty() {
125 for sib in siblings {
126 for cand in &candidates {
127 if cand.ecosystem() == sib.ecosystem() {
128 return Ok(cand.clone());
129 }
130 }
131 }
132 }
133
134 if candidates.is_empty() {
135 Err(GraphError::UnknownPackage {
136 id: PackageId::parse(name).unwrap_or_else(|_| PackageId::Bare(name.to_string())),
137 })
138 } else {
139 Err(GraphError::AmbiguousName {
140 name: name.to_string(),
141 candidates,
142 })
143 }
144 }
145
146 pub fn resolve_native(&self, eco: Ecosystem, name: &str) -> Option<&PackageId> {
147 self.native.get(&(eco, name.to_string()))
148 }
149
150 pub fn native_name(&self, id: &PackageId, eco: Ecosystem) -> Option<&str> {
151 for ((e, name), registered_id) in &self.native {
152 if e == &eco && registered_id == id {
153 return Some(name.as_str());
154 }
155 }
156 None
157 }
158
159 pub fn native_names(&self, id: &PackageId) -> impl Iterator<Item = (Ecosystem, &str)> {
160 let mut results = Vec::new();
161 for ((e, name), registered_id) in &self.native {
162 if registered_id == id {
163 results.push((*e, name.as_str()));
164 }
165 }
166 results.into_iter()
167 }
168
169 pub fn display_form(&self, id: &PackageId) -> String {
170 id.display_name()
171 }
172
173 pub fn platforms_of(&self, owner: &PackageId) -> impl Iterator<Item = (&str, &Path)> {
174 let mut results = Vec::new();
175 for (name, (plat_owner, path)) in &self.platform {
176 if plat_owner == owner {
177 results.push((name.as_str(), path.as_path()));
178 }
179 }
180 results.into_iter()
181 }
182}