Skip to main content

kellnr_common/
crate_dependents.rs

1use serde::{Deserialize, Serialize};
2use utoipa::ToSchema;
3
4use crate::crate_data::is_outdated;
5use crate::index_metadata::IndexDep;
6
7/// Crates in the registry whose latest version depends on a crate.
8#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
9pub struct CrateDependents {
10    // Latest version of the crate that is not yanked, None if there is none.
11    pub latest_version: Option<String>,
12    // Dependents the caller may see, sorted by name.
13    pub dependents: Vec<CrateDependent>,
14    // Number of dependents the caller cannot access. Only the number is
15    // shown, nothing else about them.
16    pub restricted: u64,
17}
18
19/// A crate whose latest version depends on another crate.
20#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
21pub struct CrateDependent {
22    pub name: String,
23    // Latest version of the dependent that is not yanked
24    pub version: String,
25    // How the dependent depends on the crate. Usually one entry, more if it
26    // uses the crate e.g. as normal and as dev dependency.
27    pub requirements: Vec<DependentRequirement>,
28    // Whether any requirement does not allow the latest version of the crate
29    pub outdated: bool,
30}
31
32#[derive(
33    Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, ToSchema,
34)]
35pub struct DependentRequirement {
36    // The semver requirement, e.g. "^1.2"
37    pub version_req: String,
38    // "normal", "build" or "dev"
39    pub kind: String,
40    // Whether the latest version of the crate is newer than the requirement allows
41    pub outdated: bool,
42}
43
44/// A dependency on a crate of the same registry. These are stored for every
45/// crate version to find the dependents of a crate.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct LocalDependency {
48    // Normalized package name, the original one for renamed dependencies
49    pub name: String,
50    pub version_req: String,
51    // "normal", "build" or "dev"
52    pub kind: String,
53}
54
55impl LocalDependency {
56    /// `None` for dependencies from other registries, like crates.io.
57    pub fn from_index_dep(dep: &IndexDep) -> Option<Self> {
58        if dep.registry.is_some() {
59            return None;
60        }
61        Some(LocalDependency {
62            name: dep.package.as_ref().unwrap_or(&dep.name).to_lowercase(),
63            version_req: dep.req.clone(),
64            kind: dep
65                .kind
66                .as_ref()
67                .map_or_else(|| "normal".to_string(), ToString::to_string),
68        })
69    }
70}
71
72impl CrateDependent {
73    /// The dependent `name` in `version`, which depends on a crate with the
74    /// `(version_req, kind)` pairs in `requirements`. `latest_version` is the
75    /// latest version of that crate.
76    pub fn new(
77        name: String,
78        version: String,
79        requirements: impl IntoIterator<Item = (String, String)>,
80        latest_version: Option<&str>,
81    ) -> Self {
82        let mut requirements: Vec<DependentRequirement> = requirements
83            .into_iter()
84            .map(|(version_req, kind)| DependentRequirement {
85                outdated: latest_version.is_some_and(|latest| is_outdated(&version_req, latest)),
86                version_req,
87                kind,
88            })
89            .collect();
90        // Target specific dependencies can repeat the same requirement
91        requirements.sort();
92        requirements.dedup();
93        CrateDependent {
94            name,
95            version,
96            outdated: requirements.iter().any(|r| r.outdated),
97            requirements,
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::index_metadata::DependencyKind;
106
107    fn dep(name: &str, req: &str, kind: Option<DependencyKind>) -> IndexDep {
108        IndexDep {
109            name: name.to_string(),
110            req: req.to_string(),
111            features: vec![],
112            optional: false,
113            default_features: true,
114            target: None,
115            kind,
116            registry: None,
117            package: None,
118        }
119    }
120
121    fn dependent(requirements: &[(&str, &str)], latest: Option<&str>) -> CrateDependent {
122        CrateDependent::new(
123            "bar".to_string(),
124            "1.0.0".to_string(),
125            requirements
126                .iter()
127                .map(|(req, kind)| ((*req).to_string(), (*kind).to_string())),
128            latest,
129        )
130    }
131
132    #[test]
133    fn dependent_on_old_major_is_outdated() {
134        let dependent = dependent(&[("^1", "normal")], Some("2.0.0"));
135        assert_eq!("bar", dependent.name);
136        assert_eq!("1.0.0", dependent.version);
137        assert!(dependent.outdated);
138        assert_eq!(
139            vec![DependentRequirement {
140                version_req: "^1".to_string(),
141                kind: "normal".to_string(),
142                outdated: true,
143            }],
144            dependent.requirements
145        );
146    }
147
148    #[test]
149    fn dependent_on_latest_is_not_outdated() {
150        let dependent = dependent(&[("^2", "normal")], Some("2.0.0"));
151        assert!(!dependent.outdated);
152    }
153
154    #[test]
155    fn dependent_is_outdated_if_any_requirement_is() {
156        let dependent = dependent(
157            &[("^2", "normal"), ("^1", "dev"), ("^1", "dev")],
158            Some("2.0.0"),
159        );
160        assert!(dependent.outdated);
161        assert_eq!(2, dependent.requirements.len());
162        assert_eq!("dev", dependent.requirements[0].kind);
163        assert!(!dependent.requirements[1].outdated);
164    }
165
166    #[test]
167    fn without_latest_version_nothing_is_outdated() {
168        assert!(!dependent(&[("^1", "normal")], None).outdated);
169    }
170
171    #[test]
172    fn local_dependency_uses_normalized_package_name() {
173        let mut renamed = dep("foo_renamed", "^2", Some(DependencyKind::Dev));
174        renamed.package = Some("Foo".to_string());
175        assert_eq!(
176            Some(LocalDependency {
177                name: "foo".to_string(),
178                version_req: "^2".to_string(),
179                kind: "dev".to_string(),
180            }),
181            LocalDependency::from_index_dep(&renamed)
182        );
183    }
184
185    #[test]
186    fn local_dependency_without_kind_is_normal() {
187        let local = LocalDependency::from_index_dep(&dep("foo", "^1", None)).unwrap();
188        assert_eq!("normal", local.kind);
189    }
190
191    #[test]
192    fn dependency_from_other_registry_is_not_local() {
193        let mut cratesio = dep("foo", "^1", None);
194        cratesio.registry = Some("https://github.com/rust-lang/crates.io-index".to_string());
195        assert_eq!(None, LocalDependency::from_index_dep(&cratesio));
196    }
197}