Skip to content

Commit ae50554

Browse files
committed
Auto merge of #4548 - teozkr:feature/minimal-crate-metadata-api, r=jtgeibel
Add a "minimal crate metadata" API endpoint As mentioned in #4503 and renovatebot/renovate#3486, this is intended to be a lighter-weight API than `/crates/:crate_id`, which can be subject to looser rate limits. This adds a new `/crates/:crate_id/crate` endpoint for metadata about the crate itself, without information about related objects (such as versions or categories).
2 parents 11ef962 + fae83f1 commit ae50554

File tree

8 files changed

+259
-80
lines changed

8 files changed

+259
-80
lines changed

src/controllers/krate/metadata.rs

Lines changed: 169 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//! `Cargo.toml` file.
66
77
use std::cmp::Reverse;
8+
use std::str::FromStr;
89

910
use crate::controllers::frontend_prelude::*;
1011
use crate::controllers::helpers::pagination::PaginationOptions;
@@ -45,7 +46,7 @@ pub fn summary(req: &mut dyn RequestExt) -> EndpointResult {
4546
.map(|((top_versions, krate), recent_downloads)| {
4647
Ok(EncodableCrate::from_minimal(
4748
krate,
48-
&top_versions,
49+
Some(&top_versions),
4950
None,
5051
false,
5152
recent_downloads,
@@ -111,71 +112,183 @@ pub fn summary(req: &mut dyn RequestExt) -> EndpointResult {
111112
/// Handles the `GET /crates/:crate_id` route.
112113
pub fn show(req: &mut dyn RequestExt) -> EndpointResult {
113114
let name = &req.params()["crate_id"];
115+
let include = req
116+
.query()
117+
.get("include")
118+
.map(|mode| ShowIncludeMode::from_str(mode))
119+
.transpose()?
120+
.unwrap_or_default();
121+
114122
let conn = req.db_read_only()?;
115123
let krate: Crate = Crate::by_name(name).first(&*conn)?;
116124

117-
let mut versions_and_publishers: Vec<(Version, Option<User>)> = krate
118-
.all_versions()
119-
.left_outer_join(users::table)
120-
.select((versions::all_columns, users::all_columns.nullable()))
121-
.load(&*conn)?;
122-
123-
versions_and_publishers
124-
.sort_by_cached_key(|(version, _)| Reverse(semver::Version::parse(&version.num).ok()));
125-
126-
let versions = versions_and_publishers
127-
.iter()
128-
.map(|(v, _)| v)
129-
.cloned()
130-
.collect::<Vec<_>>();
131-
let versions_publishers_and_audit_actions = versions_and_publishers
132-
.into_iter()
133-
.zip(VersionOwnerAction::for_versions(&conn, &versions)?.into_iter())
134-
.map(|((v, pb), aas)| (v, pb, aas))
135-
.collect::<Vec<_>>();
125+
let versions_publishers_and_audit_actions = if include.versions {
126+
let mut versions_and_publishers: Vec<(Version, Option<User>)> = krate
127+
.all_versions()
128+
.left_outer_join(users::table)
129+
.select((versions::all_columns, users::all_columns.nullable()))
130+
.load(&*conn)?;
131+
versions_and_publishers
132+
.sort_by_cached_key(|(version, _)| Reverse(semver::Version::parse(&version.num).ok()));
133+
134+
let versions = versions_and_publishers
135+
.iter()
136+
.map(|(v, _)| v)
137+
.cloned()
138+
.collect::<Vec<_>>();
139+
Some(
140+
versions_and_publishers
141+
.into_iter()
142+
.zip(VersionOwnerAction::for_versions(&conn, &versions)?.into_iter())
143+
.map(|((v, pb), aas)| (v, pb, aas))
144+
.collect::<Vec<_>>(),
145+
)
146+
} else {
147+
None
148+
};
136149
let ids = versions_publishers_and_audit_actions
137-
.iter()
138-
.map(|v| v.0.id)
139-
.collect();
140-
141-
let kws = CrateKeyword::belonging_to(&krate)
142-
.inner_join(keywords::table)
143-
.select(keywords::all_columns)
144-
.load(&*conn)?;
145-
let cats = CrateCategory::belonging_to(&krate)
146-
.inner_join(categories::table)
147-
.select(categories::all_columns)
148-
.load(&*conn)?;
149-
let recent_downloads = RecentCrateDownloads::belonging_to(&krate)
150-
.select(recent_crate_downloads::downloads)
151-
.get_result(&*conn)
152-
.optional()?;
150+
.as_ref()
151+
.map(|vps| vps.iter().map(|v| v.0.id).collect());
152+
153+
let kws = if include.keywords {
154+
Some(
155+
CrateKeyword::belonging_to(&krate)
156+
.inner_join(keywords::table)
157+
.select(keywords::all_columns)
158+
.load(&*conn)?,
159+
)
160+
} else {
161+
None
162+
};
163+
let cats = if include.categories {
164+
Some(
165+
CrateCategory::belonging_to(&krate)
166+
.inner_join(categories::table)
167+
.select(categories::all_columns)
168+
.load(&*conn)?,
169+
)
170+
} else {
171+
None
172+
};
173+
let recent_downloads = if include.downloads {
174+
RecentCrateDownloads::belonging_to(&krate)
175+
.select(recent_crate_downloads::downloads)
176+
.get_result(&*conn)
177+
.optional()?
178+
} else {
179+
None
180+
};
153181

154-
let badges = badges::table
155-
.filter(badges::crate_id.eq(krate.id))
156-
.load(&*conn)?;
157-
let top_versions = krate.top_versions(&conn)?;
182+
let badges = if include.badges {
183+
Some(
184+
badges::table
185+
.filter(badges::crate_id.eq(krate.id))
186+
.load(&*conn)?,
187+
)
188+
} else {
189+
None
190+
};
191+
let top_versions = if include.versions {
192+
Some(krate.top_versions(&conn)?)
193+
} else {
194+
None
195+
};
158196

159-
Ok(req.json(&json!({
160-
"crate": EncodableCrate::from(
161-
krate.clone(),
162-
&top_versions,
163-
Some(ids),
164-
Some(&kws),
165-
Some(&cats),
166-
Some(badges),
167-
false,
168-
recent_downloads,
169-
),
170-
"versions": versions_publishers_and_audit_actions
171-
.into_iter()
197+
let encodable_crate = EncodableCrate::from(
198+
krate.clone(),
199+
top_versions.as_ref(),
200+
ids,
201+
kws.as_deref(),
202+
cats.as_deref(),
203+
badges,
204+
false,
205+
recent_downloads,
206+
);
207+
let encodable_versions = versions_publishers_and_audit_actions.map(|vpa| {
208+
vpa.into_iter()
172209
.map(|(v, pb, aas)| EncodableVersion::from(v, &krate.name, pb, aas))
173-
.collect::<Vec<_>>(),
174-
"keywords": kws.into_iter().map(Keyword::into).collect::<Vec<EncodableKeyword>>(),
175-
"categories": cats.into_iter().map(Category::into).collect::<Vec<EncodableCategory>>(),
210+
.collect::<Vec<_>>()
211+
});
212+
let encodable_keywords = kws.map(|kws| {
213+
kws.into_iter()
214+
.map(Keyword::into)
215+
.collect::<Vec<EncodableKeyword>>()
216+
});
217+
let encodable_cats = cats.map(|cats| {
218+
cats.into_iter()
219+
.map(Category::into)
220+
.collect::<Vec<EncodableCategory>>()
221+
});
222+
Ok(req.json(&json!({
223+
"crate": encodable_crate,
224+
"versions": encodable_versions,
225+
"keywords": encodable_keywords,
226+
"categories": encodable_cats,
176227
})))
177228
}
178229

230+
#[derive(Debug)]
231+
struct ShowIncludeMode {
232+
versions: bool,
233+
keywords: bool,
234+
categories: bool,
235+
badges: bool,
236+
downloads: bool,
237+
}
238+
239+
impl Default for ShowIncludeMode {
240+
fn default() -> Self {
241+
// Send everything for legacy clients that expect the full response
242+
Self {
243+
versions: true,
244+
keywords: true,
245+
categories: true,
246+
badges: true,
247+
downloads: true,
248+
}
249+
}
250+
}
251+
252+
impl ShowIncludeMode {
253+
const INVALID_COMPONENT: &'static str =
254+
"invalid component for ?include= (expected 'versions', 'keywords', 'categories', 'badges', 'downloads', or 'full')";
255+
}
256+
257+
impl FromStr for ShowIncludeMode {
258+
type Err = Box<dyn AppError>;
259+
260+
fn from_str(s: &str) -> Result<Self, Self::Err> {
261+
let mut mode = Self {
262+
versions: false,
263+
keywords: false,
264+
categories: false,
265+
badges: false,
266+
downloads: false,
267+
};
268+
for component in s.split(',') {
269+
match component {
270+
"" => {}
271+
"full" => {
272+
mode = Self {
273+
versions: true,
274+
keywords: true,
275+
categories: true,
276+
badges: true,
277+
downloads: true,
278+
}
279+
}
280+
"versions" => mode.versions = true,
281+
"keywords" => mode.keywords = true,
282+
"categories" => mode.categories = true,
283+
"badges" => mode.badges = true,
284+
"downloads" => mode.downloads = true,
285+
_ => return Err(bad_request(Self::INVALID_COMPONENT)),
286+
}
287+
}
288+
Ok(mode)
289+
}
290+
}
291+
179292
/// Handles the `GET /crates/:crate_id/:version/readme` route.
180293
pub fn readme(req: &mut dyn RequestExt) -> EndpointResult {
181294
let crate_name = &req.params()["crate_id"];

src/controllers/krate/publish.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ pub fn publish(req: &mut dyn RequestExt) -> EndpointResult {
253253
};
254254

255255
Ok(req.json(&GoodCrate {
256-
krate: EncodableCrate::from_minimal(krate, &top_versions, None, false, None),
256+
krate: EncodableCrate::from_minimal(krate, Some(&top_versions), None, false, None),
257257
warnings,
258258
}))
259259
})

src/controllers/krate/search.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ pub fn search(req: &mut dyn RequestExt) -> EndpointResult {
335335
|((((max_version, krate), perfect_match), recent_downloads), badges)| {
336336
EncodableCrate::from_minimal(
337337
krate,
338-
&max_version,
338+
Some(&max_version),
339339
Some(badges),
340340
perfect_match,
341341
Some(recent_downloads),

src/tests/all.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ struct CrateMeta {
6969
pub struct CrateResponse {
7070
#[serde(rename = "crate")]
7171
krate: EncodableCrate,
72-
versions: Vec<EncodableVersion>,
73-
keywords: Vec<EncodableKeyword>,
72+
versions: Option<Vec<EncodableVersion>>,
73+
keywords: Option<Vec<EncodableKeyword>>,
7474
}
7575
#[derive(Deserialize)]
7676
pub struct VersionResponse {

src/tests/krate/show.rs

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,31 +43,79 @@ fn show() {
4343
assert_eq!(json.krate.documentation, krate.documentation);
4444
assert_eq!(json.krate.keywords, Some(vec!["kw1".into()]));
4545
assert_eq!(json.krate.recent_downloads, Some(10));
46-
let versions = json.krate.versions.as_ref().unwrap();
46+
let crate_versions = json.krate.versions.as_ref().unwrap();
47+
assert_eq!(crate_versions.len(), 3);
48+
let versions = json.versions.as_ref().unwrap();
4749
assert_eq!(versions.len(), 3);
48-
assert_eq!(json.versions.len(), 3);
4950

50-
assert_eq!(json.versions[0].id, versions[0]);
51-
assert_eq!(json.versions[0].krate, json.krate.id);
52-
assert_eq!(json.versions[0].num, "1.0.0");
53-
assert_none!(&json.versions[0].published_by);
51+
assert_eq!(versions[0].id, crate_versions[0]);
52+
assert_eq!(versions[0].krate, json.krate.id);
53+
assert_eq!(versions[0].num, "1.0.0");
54+
assert_none!(&versions[0].published_by);
5455
let suffix = "/api/v1/crates/foo_show/1.0.0/download";
5556
assert!(
56-
json.versions[0].dl_path.ends_with(suffix),
57+
versions[0].dl_path.ends_with(suffix),
5758
"bad suffix {}",
58-
json.versions[0].dl_path
59+
versions[0].dl_path
5960
);
60-
assert_eq!(1, json.keywords.len());
61-
assert_eq!("kw1", json.keywords[0].id);
61+
let keywords = json.keywords.as_ref().unwrap();
62+
assert_eq!(1, keywords.len());
63+
assert_eq!("kw1", keywords[0].id);
6264

63-
assert_eq!(json.versions[1].num, "0.5.1");
64-
assert_eq!(json.versions[2].num, "0.5.0");
65+
assert_eq!(versions[1].num, "0.5.1");
66+
assert_eq!(versions[2].num, "0.5.0");
6567
assert_eq!(
66-
json.versions[1].published_by.as_ref().unwrap().login,
68+
versions[1].published_by.as_ref().unwrap().login,
6769
user.gh_login
6870
);
6971
}
7072

73+
#[test]
74+
fn show_minimal() {
75+
let (app, anon, user) = TestApp::init().with_user();
76+
let user = user.as_model();
77+
78+
let krate = app.db(|conn| {
79+
use cargo_registry::schema::versions;
80+
use diesel::{update, ExpressionMethods};
81+
82+
let krate = CrateBuilder::new("foo_show_minimal", user.id)
83+
.description("description")
84+
.documentation("https://example.com")
85+
.homepage("http://example.com")
86+
.version(VersionBuilder::new("1.0.0"))
87+
.version(VersionBuilder::new("0.5.0"))
88+
.version(VersionBuilder::new("0.5.1"))
89+
.keyword("kw1")
90+
.downloads(20)
91+
.recent_downloads(10)
92+
.expect_build(conn);
93+
94+
// Make version 1.0.0 mimic a version published before we started recording who published
95+
// versions
96+
let none: Option<i32> = None;
97+
update(versions::table)
98+
.filter(versions::num.eq("1.0.0"))
99+
.set(versions::published_by.eq(none))
100+
.execute(conn)
101+
.unwrap();
102+
103+
krate
104+
});
105+
106+
let json = anon.show_crate_minimal("foo_show_minimal");
107+
assert_eq!(json.krate.name, krate.name);
108+
assert_eq!(json.krate.id, krate.name);
109+
assert_eq!(json.krate.description, krate.description);
110+
assert_eq!(json.krate.homepage, krate.homepage);
111+
assert_eq!(json.krate.documentation, krate.documentation);
112+
assert_eq!(json.krate.keywords, None);
113+
assert_eq!(json.krate.recent_downloads, None);
114+
assert_eq!(json.krate.versions, None);
115+
assert!(json.versions.is_none());
116+
assert!(json.keywords.is_none());
117+
}
118+
71119
#[test]
72120
fn block_bad_documentation_url() {
73121
let (app, anon, user) = TestApp::init().with_user();

0 commit comments

Comments
 (0)