Skip to main content

callisto_graph/
identity.rs

1use 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}