Skip to content

Commit 1b1be41

Browse files
committed
controllers/krate/metadata: Add support for default_version include mode
This allows us to respond with a version data of `default_version` included, which potentially benefits apps by eliminating the need to wait for the crate response to obtain the default version and then make a subsequent request to get the actual version data. This is particularly useful when we move towards not requesting with `versions` included.
1 parent f027ea1 commit 1b1be41

7 files changed

+147
-2
lines changed

src/controllers/krate/metadata.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub struct FindQueryParams {
2828
/// Additional data to include in the response.
2929
///
3030
/// Valid values: `versions`, `keywords`, `categories`, `badges`,
31-
/// `downloads`, or `full`.
31+
/// `downloads`, `default_version`, or `full`.
3232
///
3333
/// Defaults to `full` for backwards compatibility.
3434
///
@@ -118,6 +118,21 @@ pub async fn find_crate(
118118
.as_ref()
119119
.map(|vps| vps.iter().map(|v| v.0.id).collect());
120120

121+
let mut default_version_meta = None;
122+
if let Some(default_version) = default_version.as_ref().filter(|_| include.default_version) {
123+
// Find the default_version from composed versions to minimize round trips when possible.
124+
if let Some(versions) = versions_publishers_and_audit_actions.as_ref() {
125+
default_version_meta = versions
126+
.iter()
127+
.find(|(version, _, _)| version.num.as_str() == default_version)
128+
.cloned();
129+
} else if let Ok(version) = krate.find_version(&mut conn, default_version).await {
130+
let published_by = version.published_by(&mut conn).await?;
131+
let actions = VersionOwnerAction::by_version(&mut conn, &version).await?;
132+
default_version_meta = Some((version, published_by, actions));
133+
}
134+
};
135+
121136
let kws = if include.keywords {
122137
Some(
123138
CrateKeyword::belonging_to(&krate)
@@ -174,6 +189,8 @@ pub async fn find_crate(
174189
.map(|(v, pb, aas)| EncodableVersion::from(v, &krate.name, pb, aas))
175190
.collect::<Vec<_>>()
176191
});
192+
let encodable_default_version =
193+
default_version_meta.map(|(v, pb, aas)| EncodableVersion::from(v, &krate.name, pb, aas));
177194

178195
let encodable_keywords = kws.map(|kws| {
179196
kws.into_iter()
@@ -192,6 +209,7 @@ pub async fn find_crate(
192209
"versions": encodable_versions,
193210
"keywords": encodable_keywords,
194211
"categories": encodable_cats,
212+
"default_version": encodable_default_version,
195213
}))
196214
}
197215

@@ -202,6 +220,7 @@ struct ShowIncludeMode {
202220
categories: bool,
203221
badges: bool,
204222
downloads: bool,
223+
default_version: bool,
205224
}
206225

207226
impl Default for ShowIncludeMode {
@@ -213,13 +232,14 @@ impl Default for ShowIncludeMode {
213232
categories: true,
214233
badges: true,
215234
downloads: true,
235+
default_version: false,
216236
}
217237
}
218238
}
219239

220240
impl ShowIncludeMode {
221241
const INVALID_COMPONENT: &'static str =
222-
"invalid component for ?include= (expected 'versions', 'keywords', 'categories', 'badges', 'downloads', or 'full')";
242+
"invalid component for ?include= (expected 'versions', 'keywords', 'categories', 'badges', 'downloads', 'default_version', or 'full')";
223243
}
224244

225245
impl FromStr for ShowIncludeMode {
@@ -232,6 +252,7 @@ impl FromStr for ShowIncludeMode {
232252
categories: false,
233253
badges: false,
234254
downloads: false,
255+
default_version: false,
235256
};
236257
for component in s.split(',') {
237258
match component {
@@ -243,13 +264,15 @@ impl FromStr for ShowIncludeMode {
243264
categories: true,
244265
badges: true,
245266
downloads: true,
267+
default_version: true,
246268
}
247269
}
248270
"versions" => mode.versions = true,
249271
"keywords" => mode.keywords = true,
250272
"categories" => mode.categories = true,
251273
"badges" => mode.badges = true,
252274
"downloads" => mode.downloads = true,
275+
"default_version" => mode.default_version = true,
253276
_ => return Err(bad_request(Self::INVALID_COMPONENT)),
254277
}
255278
}

src/tests/routes/crates/read.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,43 @@ async fn test_new_name() {
178178
".crate.updated_at" => "[datetime]",
179179
});
180180
}
181+
182+
#[tokio::test(flavor = "multi_thread")]
183+
async fn test_include_default_version() {
184+
let (app, anon, user) = TestApp::init().with_user().await;
185+
let mut conn = app.db_conn().await;
186+
let user = user.as_model();
187+
188+
CrateBuilder::new("foo_default_version", user.id)
189+
.description("description")
190+
.documentation("https://example.com")
191+
.homepage("http://example.com")
192+
.version(VersionBuilder::new("1.0.0").yanked(true))
193+
.version(VersionBuilder::new("0.5.0"))
194+
.version(VersionBuilder::new("0.5.1"))
195+
.keyword("kw1")
196+
.downloads(20)
197+
.recent_downloads(10)
198+
.expect_build(&mut conn)
199+
.await;
200+
201+
let response = anon
202+
.get::<()>("/api/v1/crates/foo_default_version?include=default_version")
203+
.await;
204+
assert_eq!(response.status(), StatusCode::OK);
205+
assert_json_snapshot!(response.json(), {
206+
".crate.created_at" => "[datetime]",
207+
".crate.updated_at" => "[datetime]",
208+
".default_version.created_at" => "[datetime]",
209+
".default_version.updated_at" => "[datetime]",
210+
});
211+
212+
let response_with_versions = anon
213+
.get::<()>("/api/v1/crates/foo_default_version?include=versions,default_version")
214+
.await;
215+
assert_eq!(response_with_versions.status(), StatusCode::OK);
216+
assert_eq!(
217+
response_with_versions.json().get("default_version"),
218+
response.json().get("default_version")
219+
);
220+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
source: src/tests/routes/crates/read.rs
3+
expression: response.json()
4+
snapshot_kind: text
5+
---
6+
{
7+
"categories": null,
8+
"crate": {
9+
"badges": [],
10+
"categories": null,
11+
"created_at": "[datetime]",
12+
"default_version": "0.5.1",
13+
"description": "description",
14+
"documentation": "https://example.com",
15+
"downloads": 20,
16+
"exact_match": false,
17+
"homepage": "http://example.com",
18+
"id": "foo_default_version",
19+
"keywords": null,
20+
"links": {
21+
"owner_team": "/api/v1/crates/foo_default_version/owner_team",
22+
"owner_user": "/api/v1/crates/foo_default_version/owner_user",
23+
"owners": "/api/v1/crates/foo_default_version/owners",
24+
"reverse_dependencies": "/api/v1/crates/foo_default_version/reverse_dependencies",
25+
"version_downloads": "/api/v1/crates/foo_default_version/downloads",
26+
"versions": "/api/v1/crates/foo_default_version/versions"
27+
},
28+
"max_stable_version": null,
29+
"max_version": "0.0.0",
30+
"name": "foo_default_version",
31+
"newest_version": "0.0.0",
32+
"recent_downloads": null,
33+
"repository": null,
34+
"updated_at": "[datetime]",
35+
"versions": null,
36+
"yanked": false
37+
},
38+
"default_version": {
39+
"audit_actions": [],
40+
"bin_names": null,
41+
"checksum": " ",
42+
"crate": "foo_default_version",
43+
"crate_size": 0,
44+
"created_at": "[datetime]",
45+
"description": null,
46+
"dl_path": "/api/v1/crates/foo_default_version/0.5.1/download",
47+
"documentation": null,
48+
"downloads": 0,
49+
"edition": null,
50+
"features": {},
51+
"has_lib": null,
52+
"homepage": null,
53+
"id": 3,
54+
"lib_links": null,
55+
"license": null,
56+
"links": {
57+
"authors": "/api/v1/crates/foo_default_version/0.5.1/authors",
58+
"dependencies": "/api/v1/crates/foo_default_version/0.5.1/dependencies",
59+
"version_downloads": "/api/v1/crates/foo_default_version/0.5.1/downloads"
60+
},
61+
"num": "0.5.1",
62+
"published_by": {
63+
"avatar": null,
64+
"id": 1,
65+
"login": "foo",
66+
"name": null,
67+
"url": "https://github.com/foo"
68+
},
69+
"readme_path": "/api/v1/crates/foo_default_version/0.5.1/readme",
70+
"repository": null,
71+
"rust_version": null,
72+
"updated_at": "[datetime]",
73+
"yank_message": null,
74+
"yanked": false
75+
},
76+
"keywords": null,
77+
"versions": null
78+
}

src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__new_name.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ snapshot_kind: text
3535
"versions": null,
3636
"yanked": false
3737
},
38+
"default_version": null,
3839
"keywords": null,
3940
"versions": null
4041
}

src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ snapshot_kind: text
4141
],
4242
"yanked": false
4343
},
44+
"default_version": null,
4445
"keywords": [
4546
{
4647
"crates_cnt": 1,

src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_all_yanked.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ snapshot_kind: text
4040
],
4141
"yanked": true
4242
},
43+
"default_version": null,
4344
"keywords": [
4445
{
4546
"crates_cnt": 1,

src/tests/routes/crates/snapshots/crates_io__tests__routes__crates__read__show_minimal.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ snapshot_kind: text
3535
"versions": null,
3636
"yanked": false
3737
},
38+
"default_version": null,
3839
"keywords": null,
3940
"versions": null
4041
}

0 commit comments

Comments
 (0)