aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core/src/repository_utils.rs
blob: 7a797c4c16fa1e128461695842ce49a0ed739d0a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//! 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) -> &indexmap::IndexMap<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, V>(
    packages: &[P],
    requirer_requires: &indexmap::IndexMap<String, V>,
    requirer_dev_requires: Option<&indexmap::IndexMap<String, V>>,
) -> 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::*;

    struct Pkg {
        name: String,
        requires: indexmap::IndexMap<String, String>,
    }

    impl Required for Pkg {
        fn package_name(&self) -> &str {
            &self.name
        }
        fn requires(&self) -> &indexmap::IndexMap<String, String> {
            &self.requires
        }
    }

    fn pkg(name: &str, requires: &[&str]) -> Pkg {
        let mut r = indexmap::IndexMap::new();
        for n in requires {
            r.insert(n.to_string(), "*".to_string());
        }
        Pkg {
            name: name.to_string(),
            requires: r,
        }
    }

    fn root_requires(names: &[&str]) -> indexmap::IndexMap<String, String> {
        let mut m = indexmap::IndexMap::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: indexmap::IndexMap<_, ()> = indexmap::IndexMap::new();
        let kept = filter_required_packages(&packages, &root, None);
        assert!(kept.is_empty());
    }
}