aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core/src/repository_utils.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart-core/src/repository_utils.rs')
-rw-r--r--crates/mozart-core/src/repository_utils.rs174
1 files changed, 174 insertions, 0 deletions
diff --git a/crates/mozart-core/src/repository_utils.rs b/crates/mozart-core/src/repository_utils.rs
new file mode 100644
index 0000000..ecd5dd7
--- /dev/null
+++ b/crates/mozart-core/src/repository_utils.rs
@@ -0,0 +1,174 @@
+//! Mirrors `Composer\Repository\RepositoryUtils`.
+//!
+//! Currently ports `filterRequiredPackages` only; `flattenRepositories`
+//! has no Mozart equivalent yet because Mozart does not model nested
+//! `CompositeRepository`/`FilterRepository` structures.
+
+use std::collections::BTreeSet;
+
+/// Minimal contract for a package that can participate in the require
+/// closure walk performed by [`filter_required_packages`].
+///
+/// `package_name` is the normalized `vendor/name`. `requires` returns
+/// the require map (`name → constraint`); only the keys are consulted.
+/// `package_names` returns every name the package answers for —
+/// typically just `package_name`, but Composer's `PackageInterface::getNames()`
+/// also includes `provide`/`replace` targets. Implementations may
+/// return `None` when those auxiliary names are not yet modelled in
+/// Mozart's data layer; the walk falls back to matching on
+/// `package_name` only in that case.
+pub trait Required {
+ fn package_name(&self) -> &str;
+ fn requires(&self) -> &std::collections::BTreeMap<String, String>;
+ fn package_names(&self) -> Option<Vec<&str>> {
+ None
+ }
+}
+
+/// Mirror of `RepositoryUtils::filterRequiredPackages`.
+///
+/// Walks the require closure of `requirer_requires` against `packages`,
+/// collecting (in input order) every package that is reachable.
+/// `requirer_dev_requires`, when `Some`, is merged into the initial
+/// require set — matching the `$includeRequireDev` flag, which Composer
+/// only honours for the *initial* requirer (transitive walks always
+/// look at `getRequires()` only).
+///
+/// The returned vector preserves the order in which packages were
+/// discovered, matching PHP's `$bucket[] = $candidate;` push pattern.
+pub fn filter_required_packages<P>(
+ packages: &[P],
+ requirer_requires: &std::collections::BTreeMap<String, String>,
+ requirer_dev_requires: Option<&std::collections::BTreeMap<String, String>>,
+) -> Vec<usize>
+where
+ P: Required,
+{
+ let mut initial: BTreeSet<&str> = requirer_requires.keys().map(String::as_str).collect();
+ if let Some(dev) = requirer_dev_requires {
+ initial.extend(dev.keys().map(String::as_str));
+ }
+
+ let mut bucket: Vec<usize> = Vec::new();
+ walk(packages, &initial, &mut bucket);
+ bucket
+}
+
+fn walk<P>(packages: &[P], requires: &BTreeSet<&str>, bucket: &mut Vec<usize>)
+where
+ P: Required,
+{
+ for (idx, candidate) in packages.iter().enumerate() {
+ let names: Vec<&str> = candidate
+ .package_names()
+ .unwrap_or_else(|| vec![candidate.package_name()]);
+ let matches = names.iter().any(|n| requires.contains(n));
+ if !matches {
+ continue;
+ }
+ if bucket.contains(&idx) {
+ continue;
+ }
+ bucket.push(idx);
+ let next: BTreeSet<&str> = candidate.requires().keys().map(String::as_str).collect();
+ walk(packages, &next, bucket);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::collections::BTreeMap;
+
+ struct Pkg {
+ name: String,
+ requires: BTreeMap<String, String>,
+ }
+
+ impl Required for Pkg {
+ fn package_name(&self) -> &str {
+ &self.name
+ }
+ fn requires(&self) -> &BTreeMap<String, String> {
+ &self.requires
+ }
+ }
+
+ fn pkg(name: &str, requires: &[&str]) -> Pkg {
+ let mut r = BTreeMap::new();
+ for n in requires {
+ r.insert(n.to_string(), "*".to_string());
+ }
+ Pkg {
+ name: name.to_string(),
+ requires: r,
+ }
+ }
+
+ fn root_requires(names: &[&str]) -> BTreeMap<String, String> {
+ let mut m = BTreeMap::new();
+ for n in names {
+ m.insert(n.to_string(), "*".to_string());
+ }
+ m
+ }
+
+ #[test]
+ fn filters_to_root_requires_only() {
+ let packages = vec![
+ pkg("a/a", &[]),
+ pkg("b/b", &[]),
+ pkg("c/c", &[]), // not required
+ ];
+ let root = root_requires(&["a/a", "b/b"]);
+ let kept = filter_required_packages(&packages, &root, None);
+ let names: Vec<&str> = kept.iter().map(|&i| packages[i].name.as_str()).collect();
+ assert_eq!(names, vec!["a/a", "b/b"]);
+ }
+
+ #[test]
+ fn walks_transitive_requires() {
+ let packages = vec![
+ pkg("a/a", &["b/b"]),
+ pkg("b/b", &["c/c"]),
+ pkg("c/c", &[]),
+ pkg("d/d", &[]), // unreachable
+ ];
+ let root = root_requires(&["a/a"]);
+ let kept = filter_required_packages(&packages, &root, None);
+ let names: Vec<&str> = kept.iter().map(|&i| packages[i].name.as_str()).collect();
+ assert_eq!(names, vec!["a/a", "b/b", "c/c"]);
+ }
+
+ #[test]
+ fn dev_requires_only_apply_at_root() {
+ let packages = vec![
+ pkg("a/a", &[]),
+ pkg("b/b", &["c/c"]),
+ pkg("c/c", &[]), // only reachable via a's dev-requires (no dev requires here)
+ pkg("d/d", &[]),
+ ];
+ let root = root_requires(&["a/a"]);
+ let dev = root_requires(&["b/b"]);
+ let kept = filter_required_packages(&packages, &root, Some(&dev));
+ let names: Vec<&str> = kept.iter().map(|&i| packages[i].name.as_str()).collect();
+ assert_eq!(names, vec!["a/a", "b/b", "c/c"]);
+ }
+
+ #[test]
+ fn handles_circular_requires() {
+ let packages = vec![pkg("a/a", &["b/b"]), pkg("b/b", &["a/a"])];
+ let root = root_requires(&["a/a"]);
+ let kept = filter_required_packages(&packages, &root, None);
+ let names: Vec<&str> = kept.iter().map(|&i| packages[i].name.as_str()).collect();
+ assert_eq!(names, vec!["a/a", "b/b"]);
+ }
+
+ #[test]
+ fn empty_requires_yields_nothing() {
+ let packages = vec![pkg("a/a", &[]), pkg("b/b", &[])];
+ let root = BTreeMap::new();
+ let kept = filter_required_packages(&packages, &root, None);
+ assert!(kept.is_empty());
+ }
+}