Skip to main content

kellnr_common/
crate_dependents.rs

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