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#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
10pub struct CrateDependents {
11 pub latest_version: Option<String>,
13 pub dependents: Vec<CrateDependent>,
15 pub restricted: u64,
18}
19
20#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
22pub struct CrateDependent {
23 pub name: String,
24 pub version: String,
26 pub requirements: Vec<DependentRequirement>,
29 pub outdated: bool,
31}
32
33#[derive(
34 Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, ToSchema,
35)]
36pub struct DependentRequirement {
37 pub version_req: String,
39 pub kind: String,
41 pub outdated: bool,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct LocalDependency {
49 pub name: CanonicalName,
51 pub version_req: String,
52 pub kind: String,
54}
55
56impl LocalDependency {
57 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 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 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}